TvActivity.java revision 5980d6736ab06e8c15eebb151a3b0c8d943b37b8
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.Fragment; 25import android.app.FragmentTransaction; 26import android.content.ComponentName; 27import android.content.ContentUris; 28import android.content.Context; 29import android.content.Intent; 30import android.content.pm.PackageManager; 31import android.content.pm.ResolveInfo; 32import android.database.ContentObserver; 33import android.graphics.Point; 34import android.media.AudioManager; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.Handler; 38import android.provider.TvContract; 39import android.tv.TvInputInfo; 40import android.tv.TvInputManager; 41import android.tv.TvInputService; 42import android.tv.TvView; 43import android.util.Log; 44import android.view.Display; 45import android.view.GestureDetector; 46import android.view.InputEvent; 47import android.view.KeyEvent; 48import android.view.MotionEvent; 49import android.view.SurfaceHolder; 50import android.view.View; 51import android.view.ViewGroup; 52import android.widget.LinearLayout; 53import android.widget.TextView; 54import android.widget.Toast; 55 56import com.android.tv.menu.MenuDialogFragment; 57 58import java.util.List; 59 60/** 61 * The main activity for demonstrating TV app. 62 */ 63public class TvActivity extends Activity implements 64 InputPickerDialogFragment.InputPickerDialogListener, 65 AudioManager.OnAudioFocusChangeListener { 66 // STOPSHIP: Turn debugging off 67 private static final boolean DEBUG = true; 68 private static final String TAG = "TvActivity"; 69 70 private static final int DURATION_SHOW_CHANNEL_BANNER = 2000; 71 private static final int DURATION_SHOW_CONTROL_GUIDE = 1000; 72 private static final float AUDIO_MAX_VOLUME = 1.0f; 73 private static final float AUDIO_MIN_VOLUME = 0.0f; 74 private static final float AUDIO_DUCKING_VOLUME = 0.3f; 75 private static final int DELAY_FOR_SURFACE_RELEASE = 300; 76 // TODO: add more KEYCODEs to the white list. 77 private static final int[] KEYCODE_WHITELIST = { 78 KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3, 79 KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_7, 80 KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_STAR, KeyEvent.KEYCODE_POUND, 81 KeyEvent.KEYCODE_M, 82 }; 83 // TODO: this value should be able to be toggled in menu. 84 private static final boolean USE_KEYCODE_BLACKLIST = false; 85 private static final int[] KEYCODE_BLACKLIST = { 86 KeyEvent.KEYCODE_MENU, KeyEvent.KEYCODE_CHANNEL_UP, KeyEvent.KEYCODE_CHANNEL_DOWN, 87 KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT 88 }; 89 // STOPSHIP: debug keys are used only for testing. 90 private static final boolean USE_DEBUG_KEYS = true; 91 92 private static final int REQUEST_START_SETUP_ACTIIVTY = 0; 93 94 private static final String LEANBACK_SET_SHYNESS_BROADCAST = 95 "com.android.mclauncher.action.SET_APP_SHYNESS"; 96 private static final String LEANBACK_SHY_MODE_EXTRA = "shyMode"; 97 98 private TvInputManager mTvInputManager; 99 private TvView mTvView; 100 private LinearLayout mControlGuide; 101 private LinearLayout mChannelBanner; 102 private Runnable mHideChannelBanner; 103 private Runnable mHideControlGuide; 104 private TextView mChannelTextView; 105 private TextView mProgramTextView; 106 private int mShortAnimationDuration; 107 private int mDisplayWidth; 108 private GestureDetector mGestureDetector; 109 private ChannelMap mChannelMap; 110 private TvInputManager.Session mTvSession; 111 private TvInputInfo mTvInputInfo; 112 private TvInputInfo mTvInputInfoForSetup; 113 private AudioManager mAudioManager; 114 private int mAudioFocusStatus; 115 private final Handler mHandler = new Handler(); 116 private boolean mDefaultSessionRequested; 117 private boolean mTunePendding; 118 private boolean mPipShowing; 119 private boolean mDebugNonFullSizeScreen; 120 private boolean mUseKeycodeBlacklist = USE_KEYCODE_BLACKLIST; 121 private boolean mIsShy = true; 122 123 private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 124 @Override 125 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } 126 127 @Override 128 public void surfaceCreated(SurfaceHolder holder) { } 129 130 @Override 131 public void surfaceDestroyed(SurfaceHolder holder) { 132 // TODO: It is a hack to wait to release a surface at TIS. If there is a way to 133 // know when the surface is released at TIS, we don't need this hack. 134 try { 135 if (DEBUG) Log.d(TAG, "Sleep to wait destroying a surface"); 136 Thread.sleep(DELAY_FOR_SURFACE_RELEASE); 137 if (DEBUG) Log.d(TAG, "Wake up from sleeping"); 138 } catch (InterruptedException e) { 139 e.printStackTrace(); 140 } 141 } 142 }; 143 144 // PIP is used for debug/verification of multiple sessions rather than real PIP feature. 145 // When PIP is enabled, the same channel as mTvView is tuned. 146 private TvView mPipView; 147 private TvInputManager.Session mPipSession; 148 private TvInputInfo mPipInputInfo; 149 150 @Override 151 protected void onCreate(Bundle savedInstanceState) { 152 super.onCreate(savedInstanceState); 153 154 setContentView(R.layout.activity_tv); 155 mTvView = (TvView) findViewById(R.id.tv_view); 156 mTvView.setOnUnhandledInputEventListener(new TvView.OnUnhandledInputEventListener() { 157 @Override 158 public boolean onUnhandledInputEvent(InputEvent event) { 159 if (event instanceof KeyEvent) { 160 KeyEvent keyEvent = (KeyEvent) event; 161 if (keyEvent.getAction() == KeyEvent.ACTION_UP) { 162 return onKeyUp(keyEvent.getKeyCode(), keyEvent); 163 } 164 } else if (event instanceof MotionEvent) { 165 MotionEvent motionEvent = (MotionEvent) event; 166 if (motionEvent.isTouchEvent()) { 167 return onTouchEvent(motionEvent); 168 } 169 } 170 return false; 171 } 172 }); 173 mTvView.getHolder().addCallback(mSurfaceHolderCallback); 174 mPipView = (TvView) findViewById(R.id.pip_view); 175 mPipView.setZOrderMediaOverlay(true); 176 mPipView.getHolder().addCallback(mSurfaceHolderCallback); 177 178 mControlGuide = (LinearLayout) findViewById(R.id.control_guide); 179 mChannelBanner = (LinearLayout) findViewById(R.id.channel_banner); 180 181 // Initially hide the channel banner and the control guide. 182 mChannelBanner.setVisibility(View.GONE); 183 mControlGuide.setVisibility(View.GONE); 184 185 mHideControlGuide = new HideRunnable(mControlGuide); 186 mHideChannelBanner = new HideRunnable(mChannelBanner); 187 188 mChannelTextView = (TextView) findViewById(R.id.channel_text); 189 mProgramTextView = (TextView) findViewById(R.id.program_text); 190 191 mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime); 192 193 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 194 mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS; 195 Display display = getWindowManager().getDefaultDisplay(); 196 Point size = new Point(); 197 display.getSize(size); 198 mDisplayWidth = size.x; 199 200 mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { 201 static final float CONTROL_MARGIN = 0.2f; 202 final float mLeftMargin = mDisplayWidth * CONTROL_MARGIN; 203 final float mRightMargin = mDisplayWidth * (1 - CONTROL_MARGIN); 204 205 @Override 206 public boolean onDown(MotionEvent event) { 207 Log.d(TAG, "onDown: " + event.toString()); 208 if (mChannelMap == null) { 209 return false; 210 } 211 212 showAndHide(mControlGuide, mHideControlGuide, DURATION_SHOW_CONTROL_GUIDE); 213 214 if (event.getX() <= mLeftMargin) { 215 channelDown(); 216 return true; 217 } else if (event.getX() >= mRightMargin) { 218 channelUp(); 219 return true; 220 } 221 return false; 222 } 223 224 @Override 225 public boolean onSingleTapUp(MotionEvent event) { 226 if (mChannelMap == null) { 227 showInputPickerDialog(); 228 return true; 229 } 230 231 if (event.getX() > mLeftMargin && event.getX() < mRightMargin) { 232 showMenu(); 233 return true; 234 } 235 return false; 236 } 237 }); 238 239 getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, true, 240 mProgramUpdateObserver); 241 242 mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); 243 } 244 245 @Override 246 protected void onNewIntent(Intent intent) { 247 setIntent(intent); 248 } 249 250 @Override 251 protected void onResume() { 252 super.onResume(); 253 startDefaultSession(); 254 } 255 256 private void startDefaultSession() { 257 if (mTvInputManager == null) { 258 return; 259 } 260 // TODO: Remove this check after TvInputManagerService becomes system service. 261 if (mDefaultSessionRequested) { 262 return; 263 } 264 mDefaultSessionRequested = true; 265 if (mTunePendding) { 266 return; 267 } 268 269 // Check whether the system has at least one TvInputService app installed. 270 final List<ResolveInfo> services = getPackageManager().queryIntentServices( 271 new Intent(TvInputService.SERVICE_INTERFACE), PackageManager.GET_SERVICES); 272 if (services == null || services.isEmpty()) { 273 Toast.makeText(this, R.string.no_input_device_found, Toast.LENGTH_SHORT).show(); 274 // TODO: Direct the user to a Play Store landing page for TvInputService apps. 275 return; 276 } 277 278 // Figure out the initial channel to tune to. 279 long channelId = Channel.INVALID_ID; 280 Intent intent = getIntent(); 281 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 282 // In case the channel is given explicitly, use it. 283 channelId = ContentUris.parseId(intent.getData()); 284 } 285 if (channelId == Channel.INVALID_ID) { 286 // Otherwise, remember the last channel the user watched. 287 channelId = TvInputUtils.getLastWatchedChannelId(this); 288 } 289 if (channelId == Channel.INVALID_ID) { 290 // If failed to pick a channel, try a different input. 291 showInputPickerDialog(); 292 return; 293 } 294 ComponentName inputName = TvInputUtils.getInputNameForChannel(this, channelId); 295 if (inputName == null) { 296 // If failed to determine the input for that channel, try a different input. 297 showInputPickerDialog(); 298 return; 299 } 300 // If the session is already started, simply adjust the volume without tune. 301 if (mTvSession != null) { 302 setVolumeByAudioFocusStatus(); 303 } else { 304 List<TvInputInfo> inputList = mTvInputManager.getTvInputList(); 305 for (TvInputInfo info : inputList) { 306 if (inputName.equals(info.getComponent())) { 307 startSession(info, channelId); 308 break; 309 } 310 } 311 } 312 } 313 314 @Override 315 protected void onStop() { 316 if (DEBUG) Log.d(TAG, "onStop() -- stop all sessions"); 317 stopSession(); 318 stopPipSession(); 319 mDefaultSessionRequested = false; 320 if (!isShyModeSet()) { 321 setShynessMode(true); 322 } 323 super.onStop(); 324 } 325 326 public void showInputPickerDialog() { 327 InputPickerDialogFragment f = new InputPickerDialogFragment(); 328 Bundle arg = new Bundle(); 329 if (mTvInputInfo != null) { 330 arg.putString(InputPickerDialogFragment.ARG_MAIN_INPUT_ID, mTvInputInfo.getId()); 331 } 332 if (mPipInputInfo != null) { 333 arg.putString(InputPickerDialogFragment.ARG_SUB_INPUT_ID, mPipInputInfo.getId()); 334 } 335 f.setArguments(arg); 336 showDialogFragment(InputPickerDialogFragment.DIALOG_TAG, f); 337 } 338 339 @Override 340 public void onInputPicked(final TvInputInfo selectedTvInput, final String displayName) { 341 if (mTvSession != null && selectedTvInput.equals(mTvInputInfo)) { 342 // Nothing has changed thus nothing to do. 343 return; 344 } 345 346 if (!TvInputUtils.hasChannel(this, selectedTvInput)) { 347 mTvInputInfoForSetup = null; 348 if (showSetupActivity(selectedTvInput, displayName)) { 349 stopSession(); 350 } 351 return; 352 } 353 354 // Start a new session with the new input. 355 stopSession(); 356 357 // TODO: It is a hack to wait to release a surface at TIS. If there is a way to 358 // know when the surface is released at TIS, we don't need this hack. 359 mHandler.postDelayed(new Runnable() { 360 @Override 361 public void run() { 362 startSession(selectedTvInput); 363 } 364 }, DELAY_FOR_SURFACE_RELEASE); 365 } 366 367 private boolean showSetupActivity(TvInputInfo inputInfo, String displayName) { 368 PackageManager pm = getPackageManager(); 369 List<ResolveInfo> activityInfos = pm.queryIntentActivities( 370 new Intent(TvInputUtils.ACTION_SETUP), PackageManager.GET_ACTIVITIES); 371 ResolveInfo setupActivity = null; 372 if (activityInfos != null) { 373 for (ResolveInfo info : activityInfos) { 374 if (info.activityInfo.packageName.equals(inputInfo.getPackageName())) { 375 setupActivity = info; 376 } 377 } 378 } 379 380 if (setupActivity == null) { 381 String message = String.format(getString(R.string.input_setup_activity_not_found), 382 displayName); 383 new AlertDialog.Builder(this) 384 .setMessage(message) 385 .setPositiveButton(R.string.OK, null) 386 .show(); 387 return false; 388 } 389 390 mTvInputInfoForSetup = inputInfo; 391 Intent intent = new Intent(TvInputUtils.ACTION_SETUP); 392 intent.setClassName(setupActivity.activityInfo.packageName, 393 setupActivity.activityInfo.name); 394 startActivityForResult(intent, REQUEST_START_SETUP_ACTIIVTY); 395 396 return true; 397 } 398 399 @Override 400 public void onActivityResult(int requestCode, int resultCode, Intent data) { 401 switch (requestCode) { 402 case REQUEST_START_SETUP_ACTIIVTY: 403 if (resultCode == Activity.RESULT_OK && mTvInputInfoForSetup != null) { 404 startSession(mTvInputInfoForSetup); 405 } 406 break; 407 408 default: 409 //TODO: Handle failure of setup. 410 } 411 mTvInputInfoForSetup = null; 412 } 413 414 @Override 415 public boolean dispatchKeyEvent(KeyEvent event) { 416 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 417 int eventKeyCode = event.getKeyCode(); 418 if (mUseKeycodeBlacklist) { 419 for (int keycode : KEYCODE_BLACKLIST) { 420 if (keycode == eventKeyCode) { 421 return super.dispatchKeyEvent(event); 422 } 423 } 424 return dispatchKeyEventToSession(event); 425 } else { 426 for (int keycode : KEYCODE_WHITELIST) { 427 if (keycode == eventKeyCode) { 428 return dispatchKeyEventToSession(event); 429 } 430 } 431 return super.dispatchKeyEvent(event); 432 } 433 } 434 435 @Override 436 public void onAudioFocusChange(int focusChange) { 437 mAudioFocusStatus = focusChange; 438 setVolumeByAudioFocusStatus(); 439 } 440 441 private void setVolumeByAudioFocusStatus() { 442 if (mTvSession != null) { 443 switch (mAudioFocusStatus) { 444 case AudioManager.AUDIOFOCUS_GAIN: 445 mTvSession.setVolume(AUDIO_MAX_VOLUME); 446 break; 447 case AudioManager.AUDIOFOCUS_LOSS: 448 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 449 mTvSession.setVolume(AUDIO_MIN_VOLUME); 450 break; 451 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 452 mTvSession.setVolume(AUDIO_DUCKING_VOLUME); 453 break; 454 } 455 } 456 } 457 458 private void startSession(TvInputInfo selectedTvInput) { 459 long channelId = TvInputUtils.getLastWatchedChannelId(TvActivity.this, 460 selectedTvInput.getId()); 461 startSession(selectedTvInput, channelId); 462 } 463 464 private void startSession(TvInputInfo inputInfo, long channelId) { 465 // TODO: recreate SurfaceView to prevent abusing from the previous session. 466 mTvInputInfo = inputInfo; 467 // Prepare a new channel map for the current input. 468 mChannelMap = new ChannelMap(this, inputInfo.getComponent(), channelId, 469 mOnChannelsLoadFinished); 470 // Create a new session and start. 471 mTvView.bindTvInput(inputInfo.getComponent(), mSessionCreated); 472 tune(); 473 } 474 475 private final TvInputManager.SessionCreateCallback mSessionCreated = 476 new TvInputManager.SessionCreateCallback() { 477 @Override 478 public void onSessionCreated(TvInputManager.Session session) { 479 if (session != null) { 480 mTvSession = session; 481 int result = mAudioManager.requestAudioFocus(TvActivity.this, 482 AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); 483 mAudioFocusStatus = 484 (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ? 485 AudioManager.AUDIOFOCUS_GAIN 486 : AudioManager.AUDIOFOCUS_LOSS; 487 if (mTunePendding) { 488 tune(); 489 } 490 } else { 491 Log.w(TAG, "Failed to create a session"); 492 // TODO: show something to user about this error. 493 } 494 } 495 }; 496 497 private void startPipSession() { 498 if (mTvSession == null) { 499 Log.w(TAG, "TV content should be playing."); 500 return; 501 } 502 Log.d(TAG, "startPipSession"); 503 mPipInputInfo = mTvInputInfo; 504 mPipView.bindTvInput(mPipInputInfo.getComponent(), mPipSessionCreated); 505 mPipShowing = true; 506 } 507 508 private final TvInputManager.SessionCreateCallback mPipSessionCreated = 509 new TvInputManager.SessionCreateCallback() { 510 @Override 511 public void onSessionCreated(final TvInputManager.Session session) { 512 Log.d(TAG, "PIP session is created."); 513 if (mTvSession == null) { 514 Log.w(TAG, "TV content should be playing."); 515 if (session != null) { 516 mPipView.unbindTvInput(); 517 } 518 mPipShowing = false; 519 return; 520 } 521 if (session == null) { 522 Log.w(TAG, "Fail to create another session."); 523 mPipShowing = false; 524 return; 525 } 526 runOnUiThread(new Runnable() { 527 @Override 528 public void run() { 529 mPipSession = session; 530 mPipSession.setVolume(0); 531 mPipSession.tune(mChannelMap.getCurrentChannelUri()); 532 mPipView.setVisibility(View.VISIBLE); 533 } 534 }); 535 } 536 }; 537 538 private final ContentObserver mProgramUpdateObserver = new ContentObserver(new Handler()) { 539 @Override 540 public void onChange(boolean selfChange, Uri uri) { 541 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 542 return; 543 } 544 Uri channelUri = mChannelMap.getCurrentChannelUri(); 545 if (channelUri == null) { 546 return; 547 } 548 Program program = TvInputUtils.getCurrentProgram(TvActivity.this, channelUri); 549 if (program == null) { 550 return; 551 } 552 mProgramTextView.setText(program.getTitle()); 553 } 554 }; 555 556 private final Runnable mOnChannelsLoadFinished = new Runnable() { 557 @Override 558 public void run() { 559 if (mTunePendding) { 560 tune(); 561 } 562 } 563 }; 564 565 private void tune() { 566 Log.d(TAG, "tune()"); 567 // Prerequisites to be able to tune. 568 if (mChannelMap == null || !mChannelMap.isLoadFinished()) { 569 Log.d(TAG, "Channel map not ready"); 570 mTunePendding = true; 571 return; 572 } 573 if (mTvSession == null) { 574 Log.d(TAG, "Service not connected"); 575 mTunePendding = true; 576 return; 577 } 578 setVolumeByAudioFocusStatus(); 579 580 Uri currentChannelUri = mChannelMap.getCurrentChannelUri(); 581 if (currentChannelUri != null) { 582 // TODO: implement 'no signal' 583 // TODO: add result callback and show a message on failure. 584 TvInputUtils.setLastWatchedChannel(this, mTvInputInfo.getId(), currentChannelUri); 585 mTvSession.tune(currentChannelUri); 586 if (isShyModeSet()) { 587 setShynessMode(false); 588 // TODO: Set the shy mode to true when tune() fails. 589 } 590 displayChannelBanner(); 591 } 592 mTunePendding = false; 593 } 594 595 private void displayChannelBanner() { 596 runOnUiThread(new Runnable() { 597 @Override 598 public void run() { 599 // TODO: Show a beautiful channel banner instead. 600 String channelBannerString = ""; 601 String displayNumber = mChannelMap.getCurrentDisplayNumber(); 602 if (displayNumber != null) { 603 channelBannerString += displayNumber; 604 } 605 String displayName = mChannelMap.getCurrentDisplayName(); 606 if (displayName != null) { 607 channelBannerString += " " + displayName; 608 } 609 mChannelTextView.setText(channelBannerString); 610 611 Program program = TvInputUtils.getCurrentProgram(TvActivity.this, 612 mChannelMap.getCurrentChannelUri()); 613 String programTitle = program != null ? program.getTitle() : null; 614 // Program title might not be available at this point. Setting the text to null to 615 // clear the previous program title for now. It will be filled as soon as we get the 616 // updated program information. 617 mProgramTextView.setText(programTitle); 618 619 showAndHide(mChannelBanner, mHideChannelBanner, DURATION_SHOW_CHANNEL_BANNER); 620 } 621 }); 622 } 623 624 public void showRecentlyWatchedDialog() { 625 showDialogFragment(RecentlyWatchedDialogFragment.DIALOG_TAG, 626 new RecentlyWatchedDialogFragment()); 627 } 628 629 private void stopSession() { 630 if (mTvSession != null) { 631 mTvSession.setVolume(AUDIO_MIN_VOLUME); 632 mAudioManager.abandonAudioFocus(this); 633 mTvView.unbindTvInput(); 634 mTvSession = null; 635 mTvInputInfo = null; 636 } 637 if (mChannelMap != null) { 638 mChannelMap.close(); 639 mChannelMap = null; 640 } 641 } 642 643 private void stopPipSession() { 644 Log.d(TAG, "stopPipSession"); 645 if (mPipSession != null) { 646 mPipView.setVisibility(View.INVISIBLE); 647 mPipView.unbindTvInput(); 648 mPipSession = null; 649 mPipInputInfo = null; 650 } 651 mPipShowing = false; 652 } 653 654 @Override 655 protected void onSaveInstanceState(Bundle outState) { 656 // Do not save instance state because restoring instance state when TV app died 657 // unexpectedly can cause some problems like initializing fragments duplicately and 658 // accessing resource before it is initialzed. 659 } 660 661 @Override 662 protected void onDestroy() { 663 getContentResolver().unregisterContentObserver(mProgramUpdateObserver); 664 mTvView.getHolder().removeCallback(mSurfaceHolderCallback); 665 mPipView.getHolder().removeCallback(mSurfaceHolderCallback); 666 Log.d(TAG, "onDestroy()"); 667 super.onDestroy(); 668 } 669 670 @Override 671 public boolean onKeyUp(int keyCode, KeyEvent event) { 672 if (mChannelMap == null) { 673 switch (keyCode) { 674 case KeyEvent.KEYCODE_H: 675 showRecentlyWatchedDialog(); 676 return true; 677 case KeyEvent.KEYCODE_TV_INPUT: 678 case KeyEvent.KEYCODE_I: 679 case KeyEvent.KEYCODE_CHANNEL_UP: 680 case KeyEvent.KEYCODE_DPAD_UP: 681 case KeyEvent.KEYCODE_CHANNEL_DOWN: 682 case KeyEvent.KEYCODE_DPAD_DOWN: 683 case KeyEvent.KEYCODE_NUMPAD_ENTER: 684 case KeyEvent.KEYCODE_E: 685 case KeyEvent.KEYCODE_MENU: 686 showInputPickerDialog(); 687 return true; 688 } 689 } else { 690 switch (keyCode) { 691 case KeyEvent.KEYCODE_H: 692 showRecentlyWatchedDialog(); 693 return true; 694 695 case KeyEvent.KEYCODE_TV_INPUT: 696 case KeyEvent.KEYCODE_I: 697 showInputPickerDialog(); 698 return true; 699 700 case KeyEvent.KEYCODE_CHANNEL_UP: 701 case KeyEvent.KEYCODE_DPAD_UP: 702 channelUp(); 703 return true; 704 705 case KeyEvent.KEYCODE_CHANNEL_DOWN: 706 case KeyEvent.KEYCODE_DPAD_DOWN: 707 channelDown(); 708 return true; 709 710 case KeyEvent.KEYCODE_NUMPAD_ENTER: 711 case KeyEvent.KEYCODE_E: 712 displayChannelBanner(); 713 return true; 714 715 case KeyEvent.KEYCODE_DPAD_CENTER: 716 case KeyEvent.KEYCODE_MENU: 717 if (event.isCanceled()) { 718 return true; 719 } 720 showMenu(); 721 return true; 722 } 723 } 724 if (USE_DEBUG_KEYS) { 725 switch (keyCode) { 726 case KeyEvent.KEYCODE_W: { 727 mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen; 728 if (mDebugNonFullSizeScreen) { 729 mTvView.layout(100, 100, 400, 300); 730 } else { 731 ViewGroup.LayoutParams params = mTvView.getLayoutParams(); 732 params.width = ViewGroup.LayoutParams.MATCH_PARENT; 733 params.height = ViewGroup.LayoutParams.MATCH_PARENT; 734 mTvView.setLayoutParams(params); 735 } 736 return true; 737 } 738 case KeyEvent.KEYCODE_P: { 739 togglePipView(); 740 return true; 741 } 742 case KeyEvent.KEYCODE_CTRL_LEFT: 743 case KeyEvent.KEYCODE_CTRL_RIGHT: { 744 mUseKeycodeBlacklist = !mUseKeycodeBlacklist; 745 return true; 746 } 747 } 748 } 749 return super.onKeyUp(keyCode, event); 750 } 751 752 @Override 753 public boolean onTouchEvent(MotionEvent event) { 754 mGestureDetector.onTouchEvent(event); 755 return super.onTouchEvent(event); 756 } 757 758 public void togglePipView() { 759 if (mPipShowing) { 760 stopPipSession(); 761 } else { 762 startPipSession(); 763 } 764 } 765 766 private boolean dispatchKeyEventToSession(final KeyEvent event) { 767 if (DEBUG) Log.d(TAG, "dispatchKeyEventToSession(" + event + ")"); 768 if (mTvView != null) { 769 return mTvView.dispatchKeyEvent(event); 770 } 771 return false; 772 } 773 774 private void channelUp() { 775 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 776 mChannelMap.moveToNextChannel(); 777 tune(); 778 } 779 } 780 781 private void channelDown() { 782 if (mChannelMap != null && mChannelMap.isLoadFinished()) { 783 mChannelMap.moveToPreviousChannel(); 784 tune(); 785 } 786 } 787 788 private void showMenu() { 789 MenuDialogFragment f = new MenuDialogFragment(); 790 if (mTvSession != null) { 791 Bundle arg = new Bundle(); 792 arg.putParcelable(MenuDialogFragment.ARG_CURRENT_INPUT, mTvInputInfo); 793 f.setArguments(arg); 794 } 795 796 showDialogFragment(MenuDialogFragment.DIALOG_TAG, f); 797 } 798 799 private void showDialogFragment(String tag, DialogFragment dialog) { 800 FragmentTransaction ft = getFragmentManager().beginTransaction(); 801 Fragment prev = getFragmentManager().findFragmentByTag(tag); 802 if (prev != null) { 803 ft.remove(prev); 804 } 805 ft.addToBackStack(null); 806 dialog.show(ft, tag); 807 } 808 809 private final Handler mHideHandler = new Handler(); 810 811 private class HideRunnable implements Runnable { 812 private final View mView; 813 814 public HideRunnable(View view) { 815 mView = view; 816 } 817 818 @Override 819 public void run() { 820 mView.animate() 821 .alpha(0f) 822 .setDuration(mShortAnimationDuration) 823 .setListener(new AnimatorListenerAdapter() { 824 @Override 825 public void onAnimationEnd(Animator animation) { 826 mView.setVisibility(View.GONE); 827 } 828 }); 829 } 830 } 831 832 private void showAndHide(View view, Runnable hide, long duration) { 833 if (view.getVisibility() == View.VISIBLE) { 834 // Skip the show animation if the view is already visible and cancel the scheduled hide 835 // animation. 836 mHideHandler.removeCallbacks(hide); 837 } else { 838 view.setAlpha(0f); 839 view.setVisibility(View.VISIBLE); 840 view.animate() 841 .alpha(1f) 842 .setDuration(mShortAnimationDuration) 843 .setListener(null); 844 } 845 // Schedule the hide animation after a few seconds. 846 mHideHandler.postDelayed(hide, duration); 847 } 848 849 private void setShynessMode(boolean shyMode) { 850 mIsShy = shyMode; 851 Intent intent = new Intent(LEANBACK_SET_SHYNESS_BROADCAST); 852 intent.putExtra(LEANBACK_SHY_MODE_EXTRA, shyMode); 853 sendBroadcast(intent); 854 } 855 856 private boolean isShyModeSet() { 857 return mIsShy; 858 } 859} 860