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