TvView.java revision 3b9be6700fd631e25559693820d03389f8de3893
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 android.media.tv; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.annotation.RequiresPermission; 22import android.annotation.SystemApi; 23import android.content.Context; 24import android.graphics.Canvas; 25import android.graphics.PorterDuff; 26import android.graphics.Rect; 27import android.graphics.Region; 28import android.media.PlaybackParams; 29import android.media.tv.TvInputManager.Session; 30import android.media.tv.TvInputManager.Session.FinishedInputEventCallback; 31import android.media.tv.TvInputManager.SessionCallback; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.Handler; 35import android.text.TextUtils; 36import android.util.AttributeSet; 37import android.util.Log; 38import android.util.Pair; 39import android.view.InputEvent; 40import android.view.KeyEvent; 41import android.view.MotionEvent; 42import android.view.Surface; 43import android.view.SurfaceHolder; 44import android.view.SurfaceView; 45import android.view.View; 46import android.view.ViewGroup; 47import android.view.ViewRootImpl; 48 49import java.lang.ref.WeakReference; 50import java.util.ArrayDeque; 51import java.util.List; 52import java.util.Queue; 53 54/** 55 * Displays TV contents. The TvView class provides a high level interface for applications to show 56 * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of 57 * TV inputs available on the system can be obtained by calling 58 * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.) 59 * 60 * <p>Once the application supplies the URI for a specific TV channel to {@link #tune} 61 * method, it takes care of underlying service binding (and unbinding if the current TvView is 62 * already bound to a service) and automatically allocates/deallocates resources needed. In addition 63 * to a few essential methods to control how the contents are presented, it also provides a way to 64 * dispatch input events to the connected TvInputService in order to enable custom key actions for 65 * the TV input. 66 */ 67public class TvView extends ViewGroup { 68 private static final String TAG = "TvView"; 69 private static final boolean DEBUG = false; 70 71 private static final int ZORDER_MEDIA = 0; 72 private static final int ZORDER_MEDIA_OVERLAY = 1; 73 private static final int ZORDER_ON_TOP = 2; 74 75 private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null); 76 77 private static final Object sMainTvViewLock = new Object(); 78 private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW; 79 80 private final Handler mHandler = new Handler(); 81 private Session mSession; 82 private SurfaceView mSurfaceView; 83 private Surface mSurface; 84 private boolean mOverlayViewCreated; 85 private Rect mOverlayViewFrame; 86 private final TvInputManager mTvInputManager; 87 private MySessionCallback mSessionCallback; 88 private TvInputCallback mCallback; 89 private OnUnhandledInputEventListener mOnUnhandledInputEventListener; 90 private Float mStreamVolume; 91 private Boolean mCaptionEnabled; 92 private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>(); 93 94 private boolean mSurfaceChanged; 95 private int mSurfaceFormat; 96 private int mSurfaceWidth; 97 private int mSurfaceHeight; 98 private final AttributeSet mAttrs; 99 private final int mDefStyleAttr; 100 private int mWindowZOrder; 101 private boolean mUseRequestedSurfaceLayout; 102 private int mSurfaceViewLeft; 103 private int mSurfaceViewRight; 104 private int mSurfaceViewTop; 105 private int mSurfaceViewBottom; 106 private TimeShiftPositionCallback mTimeShiftPositionCallback; 107 108 private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 109 @Override 110 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 111 if (DEBUG) { 112 Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" 113 + width + ", height=" + height + ")"); 114 } 115 mSurfaceFormat = format; 116 mSurfaceWidth = width; 117 mSurfaceHeight = height; 118 mSurfaceChanged = true; 119 dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); 120 } 121 122 @Override 123 public void surfaceCreated(SurfaceHolder holder) { 124 mSurface = holder.getSurface(); 125 setSessionSurface(mSurface); 126 } 127 128 @Override 129 public void surfaceDestroyed(SurfaceHolder holder) { 130 mSurface = null; 131 mSurfaceChanged = false; 132 setSessionSurface(null); 133 } 134 }; 135 136 private final FinishedInputEventCallback mFinishedInputEventCallback = 137 new FinishedInputEventCallback() { 138 @Override 139 public void onFinishedInputEvent(Object token, boolean handled) { 140 if (DEBUG) { 141 Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); 142 } 143 if (handled) { 144 return; 145 } 146 // TODO: Re-order unhandled events. 147 InputEvent event = (InputEvent) token; 148 if (dispatchUnhandledInputEvent(event)) { 149 return; 150 } 151 ViewRootImpl viewRootImpl = getViewRootImpl(); 152 if (viewRootImpl != null) { 153 viewRootImpl.dispatchUnhandledInputEvent(event); 154 } 155 } 156 }; 157 158 public TvView(Context context) { 159 this(context, null, 0); 160 } 161 162 public TvView(Context context, AttributeSet attrs) { 163 this(context, attrs, 0); 164 } 165 166 public TvView(Context context, AttributeSet attrs, int defStyleAttr) { 167 super(context, attrs, defStyleAttr); 168 mAttrs = attrs; 169 mDefStyleAttr = defStyleAttr; 170 resetSurfaceView(); 171 mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE); 172 } 173 174 /** 175 * Sets the callback to be invoked when an event is dispatched to this TvView. 176 * 177 * @param callback The callback to receive events. A value of {@code null} removes the existing 178 * callback. 179 */ 180 public void setCallback(@Nullable TvInputCallback callback) { 181 mCallback = callback; 182 } 183 184 /** 185 * Sets this as the main {@link TvView}. 186 * 187 * <p>The main {@link TvView} is a {@link TvView} whose corresponding TV input determines the 188 * HDMI-CEC active source device. For an HDMI port input, one of source devices that is 189 * connected to that HDMI port becomes the active source. For an HDMI-CEC logical device input, 190 * the corresponding HDMI-CEC logical device becomes the active source. For any non-HDMI input 191 * (including the tuner, composite, S-Video, etc.), the internal device (= TV itself) becomes 192 * the active source. 193 * 194 * <p>First tuned {@link TvView} becomes main automatically, and keeps to be main until either 195 * {@link #reset} is called for the main {@link TvView} or {@code setMain()} is called for other 196 * {@link TvView}. 197 * @hide 198 */ 199 @SystemApi 200 public void setMain() { 201 synchronized (sMainTvViewLock) { 202 sMainTvView = new WeakReference<>(this); 203 if (hasWindowFocus() && mSession != null) { 204 mSession.setMain(); 205 } 206 } 207 } 208 209 /** 210 * Controls whether the TvView's surface is placed on top of another regular surface view in the 211 * window (but still behind the window itself). 212 * This is typically used to place overlays on top of an underlying TvView. 213 * 214 * <p>Note that this must be set before the TvView's containing window is attached to the 215 * window manager. 216 * 217 * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. 218 * 219 * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false} 220 * otherwise. 221 */ 222 public void setZOrderMediaOverlay(boolean isMediaOverlay) { 223 if (isMediaOverlay) { 224 mWindowZOrder = ZORDER_MEDIA_OVERLAY; 225 removeSessionOverlayView(); 226 } else { 227 mWindowZOrder = ZORDER_MEDIA; 228 createSessionOverlayView(); 229 } 230 if (mSurfaceView != null) { 231 // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 232 // from WindowLayoutParam as well as changes window type. 233 mSurfaceView.setZOrderOnTop(false); 234 mSurfaceView.setZOrderMediaOverlay(isMediaOverlay); 235 } 236 } 237 238 /** 239 * Controls whether the TvView's surface is placed on top of its window. Normally it is placed 240 * behind the window, to allow it to (for the most part) appear to composite with the views in 241 * the hierarchy. By setting this, you cause it to be placed above the window. This means that 242 * none of the contents of the window this TvView is in will be visible on top of its surface. 243 * 244 * <p>Note that this must be set before the TvView's containing window is attached to the window 245 * manager. 246 * 247 * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. 248 * 249 * @param onTop {@code true} to be on top of its window, {@code false} otherwise. 250 */ 251 public void setZOrderOnTop(boolean onTop) { 252 if (onTop) { 253 mWindowZOrder = ZORDER_ON_TOP; 254 removeSessionOverlayView(); 255 } else { 256 mWindowZOrder = ZORDER_MEDIA; 257 createSessionOverlayView(); 258 } 259 if (mSurfaceView != null) { 260 mSurfaceView.setZOrderMediaOverlay(false); 261 mSurfaceView.setZOrderOnTop(onTop); 262 } 263 } 264 265 /** 266 * Sets the relative stream volume of this TvView. 267 * 268 * <p>This method is primarily used to handle audio focus changes or mute a specific TvView when 269 * multiple views are displayed. If the method has not yet been called, the TvView assumes the 270 * default value of {@code 1.0f}. 271 * 272 * @param volume A volume value between {@code 0.0f} to {@code 1.0f}. 273 */ 274 public void setStreamVolume(float volume) { 275 if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")"); 276 mStreamVolume = volume; 277 if (mSession == null) { 278 // Volume will be set once the connection has been made. 279 return; 280 } 281 mSession.setStreamVolume(volume); 282 } 283 284 /** 285 * Tunes to a given channel. 286 * 287 * @param inputId The ID of the TV input for the given channel. 288 * @param channelUri The URI of a channel. 289 */ 290 public void tune(@NonNull String inputId, Uri channelUri) { 291 tune(inputId, channelUri, null); 292 } 293 294 /** 295 * Tunes to a given channel. This can be used to provide domain-specific features that are only 296 * known between certain clients and their TV inputs. 297 * 298 * @param inputId The ID of TV input for the given channel. 299 * @param channelUri The URI of a channel. 300 * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped 301 * name, i.e. prefixed with a package name you own, so that different developers will 302 * not create conflicting keys. 303 */ 304 public void tune(String inputId, Uri channelUri, Bundle params) { 305 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 306 if (TextUtils.isEmpty(inputId)) { 307 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 308 } 309 synchronized (sMainTvViewLock) { 310 if (sMainTvView.get() == null) { 311 sMainTvView = new WeakReference<>(this); 312 } 313 } 314 if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { 315 if (mSession != null) { 316 mSession.tune(channelUri, params); 317 } else { 318 // createSession() was called but the actual session for the given inputId has not 319 // yet been created. Just replace the existing tuning params in the callback with 320 // the new ones and tune later in onSessionCreated(). It is not necessary to create 321 // a new callback because this tuning request was made on the same inputId. 322 mSessionCallback.mChannelUri = channelUri; 323 mSessionCallback.mTuneParams = params; 324 } 325 } else { 326 resetInternal(); 327 // In case createSession() is called multiple times across different inputId's before 328 // any session is created (e.g. when quickly tuning to a channel from input A and then 329 // to another channel from input B), only the callback for the last createSession() 330 // should be invoked. (The previous callbacks are simply ignored.) To do that, we create 331 // a new callback each time and keep mSessionCallback pointing to the last one. If 332 // MySessionCallback.this is different from mSessionCallback, we know that this callback 333 // is obsolete and should ignore it. 334 mSessionCallback = new MySessionCallback(inputId, channelUri, params); 335 if (mTvInputManager != null) { 336 mTvInputManager.createSession(inputId, mSessionCallback, mHandler); 337 } 338 } 339 } 340 341 /** 342 * Resets this TvView. 343 * 344 * <p>This method is primarily used to un-tune the current TvView. 345 */ 346 public void reset() { 347 if (DEBUG) Log.d(TAG, "reset()"); 348 synchronized (sMainTvViewLock) { 349 if (this == sMainTvView.get()) { 350 sMainTvView = NULL_TV_VIEW; 351 } 352 } 353 resetInternal(); 354 } 355 356 private void resetInternal() { 357 mSessionCallback = null; 358 mPendingAppPrivateCommands.clear(); 359 if (mSession != null) { 360 setSessionSurface(null); 361 removeSessionOverlayView(); 362 mUseRequestedSurfaceLayout = false; 363 mSession.release(); 364 mSession = null; 365 resetSurfaceView(); 366 } 367 } 368 369 /** 370 * Requests to unblock TV content according to the given rating. 371 * 372 * <p>This notifies TV input that blocked content is now OK to play. 373 * 374 * @param unblockedRating A TvContentRating to unblock. 375 * @see TvInputService.Session#notifyContentBlocked(TvContentRating) 376 * @removed 377 */ 378 public void requestUnblockContent(TvContentRating unblockedRating) { 379 unblockContent(unblockedRating); 380 } 381 382 /** 383 * Requests to unblock TV content according to the given rating. 384 * 385 * <p>This notifies TV input that blocked content is now OK to play. 386 * 387 * @param unblockedRating A TvContentRating to unblock. 388 * @see TvInputService.Session#notifyContentBlocked(TvContentRating) 389 * @hide 390 */ 391 @SystemApi 392 @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS) 393 public void unblockContent(TvContentRating unblockedRating) { 394 if (mSession != null) { 395 mSession.unblockContent(unblockedRating); 396 } 397 } 398 399 /** 400 * Enables or disables the caption in this TvView. 401 * 402 * <p>Note that this method does not take any effect unless the current TvView is tuned. 403 * 404 * @param enabled {@code true} to enable, {@code false} to disable. 405 */ 406 public void setCaptionEnabled(boolean enabled) { 407 if (DEBUG) Log.d(TAG, "setCaptionEnabled(" + enabled + ")"); 408 mCaptionEnabled = enabled; 409 if (mSession != null) { 410 mSession.setCaptionEnabled(enabled); 411 } 412 } 413 414 /** 415 * Selects a track. 416 * 417 * @param type The type of the track to select. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 418 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 419 * @param trackId The ID of the track to select. {@code null} means to unselect the current 420 * track for a given type. 421 * @see #getTracks 422 * @see #getSelectedTrack 423 */ 424 public void selectTrack(int type, String trackId) { 425 if (mSession != null) { 426 mSession.selectTrack(type, trackId); 427 } 428 } 429 430 /** 431 * Returns the list of tracks. Returns {@code null} if the information is not available. 432 * 433 * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 434 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 435 * @see #selectTrack 436 * @see #getSelectedTrack 437 */ 438 public List<TvTrackInfo> getTracks(int type) { 439 if (mSession == null) { 440 return null; 441 } 442 return mSession.getTracks(type); 443 } 444 445 /** 446 * Returns the ID of the selected track for a given type. Returns {@code null} if the 447 * information is not available or the track is not selected. 448 * 449 * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 450 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 451 * @see #selectTrack 452 * @see #getTracks 453 */ 454 public String getSelectedTrack(int type) { 455 if (mSession == null) { 456 return null; 457 } 458 return mSession.getSelectedTrack(type); 459 } 460 461 /** 462 * Plays a given recorded TV program. 463 * 464 * @param inputId The ID of the TV input that created the given recorded program. 465 * @param recordedProgramUri The URI of a recorded program. 466 */ 467 public void timeShiftPlay(String inputId, Uri recordedProgramUri) { 468 if (DEBUG) Log.d(TAG, "timeShiftPlay(" + recordedProgramUri + ")"); 469 if (TextUtils.isEmpty(inputId)) { 470 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 471 } 472 synchronized (sMainTvViewLock) { 473 if (sMainTvView.get() == null) { 474 sMainTvView = new WeakReference<>(this); 475 } 476 } 477 if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { 478 if (mSession != null) { 479 mSession.timeShiftPlay(recordedProgramUri); 480 } else { 481 mSessionCallback.mRecordedProgramUri = recordedProgramUri; 482 } 483 } else { 484 resetInternal(); 485 mSessionCallback = new MySessionCallback(inputId, recordedProgramUri); 486 if (mTvInputManager != null) { 487 mTvInputManager.createSession(inputId, mSessionCallback, mHandler); 488 } 489 } 490 } 491 492 /** 493 * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume. 494 */ 495 public void timeShiftPause() { 496 if (mSession != null) { 497 mSession.timeShiftPause(); 498 } 499 } 500 501 /** 502 * Resumes playback. No-op if it is already resumed. Call {@link #timeShiftPause} to pause. 503 */ 504 public void timeShiftResume() { 505 if (mSession != null) { 506 mSession.timeShiftResume(); 507 } 508 } 509 510 /** 511 * Seeks to a specified time position. {@code timeMs} must be equal to or greater than the start 512 * position returned by {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged} and 513 * equal to or less than the current time. 514 * 515 * @param timeMs The time position to seek to, in milliseconds since the epoch. 516 */ 517 public void timeShiftSeekTo(long timeMs) { 518 if (mSession != null) { 519 mSession.timeShiftSeekTo(timeMs); 520 } 521 } 522 523 /** 524 * Sets playback rate using {@link android.media.PlaybackParams}. 525 * 526 * @param params The playback params. 527 */ 528 public void timeShiftSetPlaybackParams(@NonNull PlaybackParams params) { 529 if (mSession != null) { 530 mSession.timeShiftSetPlaybackParams(params); 531 } 532 } 533 534 /** 535 * Sets the callback to be invoked when the time shift position is changed. 536 * 537 * @param callback The callback to receive time shift position changes. A value of {@code null} 538 * removes the existing callback. 539 */ 540 public void setTimeShiftPositionCallback(@Nullable TimeShiftPositionCallback callback) { 541 mTimeShiftPositionCallback = callback; 542 ensurePositionTracking(); 543 } 544 545 private void ensurePositionTracking() { 546 if (mSession == null) { 547 return; 548 } 549 mSession.timeShiftEnablePositionTracking(mTimeShiftPositionCallback != null); 550 } 551 552 /** 553 * Sends a private command to the underlying TV input. This can be used to provide 554 * domain-specific features that are only known between certain clients and their TV inputs. 555 * 556 * @param action The name of the private command to send. This <em>must</em> be a scoped name, 557 * i.e. prefixed with a package name you own, so that different developers will not 558 * create conflicting commands. 559 * @param data An optional bundle to send with the command. 560 */ 561 public void sendAppPrivateCommand(@NonNull String action, Bundle data) { 562 if (TextUtils.isEmpty(action)) { 563 throw new IllegalArgumentException("action cannot be null or an empty string"); 564 } 565 if (mSession != null) { 566 mSession.sendAppPrivateCommand(action, data); 567 } else { 568 Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action 569 + "\" pending)"); 570 mPendingAppPrivateCommands.add(Pair.create(action, data)); 571 } 572 } 573 574 /** 575 * Dispatches an unhandled input event to the next receiver. 576 * 577 * <p>Except system keys, TvView always consumes input events in the normal flow. This is called 578 * asynchronously from where the event is dispatched. It gives the host application a chance to 579 * dispatch the unhandled input events. 580 * 581 * @param event The input event. 582 * @return {@code true} if the event was handled by the view, {@code false} otherwise. 583 */ 584 public boolean dispatchUnhandledInputEvent(InputEvent event) { 585 if (mOnUnhandledInputEventListener != null) { 586 if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { 587 return true; 588 } 589 } 590 return onUnhandledInputEvent(event); 591 } 592 593 /** 594 * Called when an unhandled input event also has not been handled by the user provided 595 * callback. This is the last chance to handle the unhandled input event in the TvView. 596 * 597 * @param event The input event. 598 * @return If you handled the event, return {@code true}. If you want to allow the event to be 599 * handled by the next receiver, return {@code false}. 600 */ 601 public boolean onUnhandledInputEvent(InputEvent event) { 602 return false; 603 } 604 605 /** 606 * Registers a callback to be invoked when an input event is not handled by the bound TV input. 607 * 608 * @param listener The callback to be invoked when the unhandled input event is received. 609 */ 610 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 611 mOnUnhandledInputEventListener = listener; 612 } 613 614 @Override 615 public boolean dispatchKeyEvent(KeyEvent event) { 616 if (super.dispatchKeyEvent(event)) { 617 return true; 618 } 619 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 620 if (mSession == null) { 621 return false; 622 } 623 InputEvent copiedEvent = event.copy(); 624 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 625 mHandler); 626 return ret != Session.DISPATCH_NOT_HANDLED; 627 } 628 629 @Override 630 public boolean dispatchTouchEvent(MotionEvent event) { 631 if (super.dispatchTouchEvent(event)) { 632 return true; 633 } 634 if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); 635 if (mSession == null) { 636 return false; 637 } 638 InputEvent copiedEvent = event.copy(); 639 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 640 mHandler); 641 return ret != Session.DISPATCH_NOT_HANDLED; 642 } 643 644 @Override 645 public boolean dispatchTrackballEvent(MotionEvent event) { 646 if (super.dispatchTrackballEvent(event)) { 647 return true; 648 } 649 if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); 650 if (mSession == null) { 651 return false; 652 } 653 InputEvent copiedEvent = event.copy(); 654 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 655 mHandler); 656 return ret != Session.DISPATCH_NOT_HANDLED; 657 } 658 659 @Override 660 public boolean dispatchGenericMotionEvent(MotionEvent event) { 661 if (super.dispatchGenericMotionEvent(event)) { 662 return true; 663 } 664 if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); 665 if (mSession == null) { 666 return false; 667 } 668 InputEvent copiedEvent = event.copy(); 669 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 670 mHandler); 671 return ret != Session.DISPATCH_NOT_HANDLED; 672 } 673 674 @Override 675 public void dispatchWindowFocusChanged(boolean hasFocus) { 676 super.dispatchWindowFocusChanged(hasFocus); 677 // Other app may have shown its own main TvView. 678 // Set main again to regain main session. 679 synchronized (sMainTvViewLock) { 680 if (hasFocus && this == sMainTvView.get() && mSession != null) { 681 mSession.setMain(); 682 } 683 } 684 } 685 686 @Override 687 protected void onAttachedToWindow() { 688 super.onAttachedToWindow(); 689 createSessionOverlayView(); 690 } 691 692 @Override 693 protected void onDetachedFromWindow() { 694 removeSessionOverlayView(); 695 super.onDetachedFromWindow(); 696 } 697 698 @Override 699 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 700 if (DEBUG) { 701 Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right 702 + ", bottom=" + bottom + ",)"); 703 } 704 if (mUseRequestedSurfaceLayout) { 705 mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, 706 mSurfaceViewBottom); 707 } else { 708 mSurfaceView.layout(0, 0, right - left, bottom - top); 709 } 710 } 711 712 @Override 713 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 714 mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); 715 int width = mSurfaceView.getMeasuredWidth(); 716 int height = mSurfaceView.getMeasuredHeight(); 717 int childState = mSurfaceView.getMeasuredState(); 718 setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), 719 resolveSizeAndState(height, heightMeasureSpec, 720 childState << MEASURED_HEIGHT_STATE_SHIFT)); 721 } 722 723 @Override 724 public boolean gatherTransparentRegion(Region region) { 725 if (mWindowZOrder != ZORDER_ON_TOP) { 726 if (region != null) { 727 int width = getWidth(); 728 int height = getHeight(); 729 if (width > 0 && height > 0) { 730 int location[] = new int[2]; 731 getLocationInWindow(location); 732 int left = location[0]; 733 int top = location[1]; 734 region.op(left, top, left + width, top + height, Region.Op.UNION); 735 } 736 } 737 } 738 return super.gatherTransparentRegion(region); 739 } 740 741 @Override 742 public void draw(Canvas canvas) { 743 if (mWindowZOrder != ZORDER_ON_TOP) { 744 // Punch a hole so that the underlying overlay view and surface can be shown. 745 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 746 } 747 super.draw(canvas); 748 } 749 750 @Override 751 protected void dispatchDraw(Canvas canvas) { 752 if (mWindowZOrder != ZORDER_ON_TOP) { 753 // Punch a hole so that the underlying overlay view and surface can be shown. 754 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 755 } 756 super.dispatchDraw(canvas); 757 } 758 759 @Override 760 protected void onVisibilityChanged(View changedView, int visibility) { 761 super.onVisibilityChanged(changedView, visibility); 762 mSurfaceView.setVisibility(visibility); 763 if (visibility == View.VISIBLE) { 764 createSessionOverlayView(); 765 } else { 766 removeSessionOverlayView(); 767 } 768 } 769 770 private void resetSurfaceView() { 771 if (mSurfaceView != null) { 772 mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); 773 removeView(mSurfaceView); 774 } 775 mSurface = null; 776 mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { 777 @Override 778 protected void updateWindow(boolean force, boolean redrawNeeded) { 779 super.updateWindow(force, redrawNeeded); 780 relayoutSessionOverlayView(); 781 }}; 782 mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); 783 if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) { 784 mSurfaceView.setZOrderMediaOverlay(true); 785 } else if (mWindowZOrder == ZORDER_ON_TOP) { 786 mSurfaceView.setZOrderOnTop(true); 787 } 788 addView(mSurfaceView); 789 } 790 791 private void setSessionSurface(Surface surface) { 792 if (mSession == null) { 793 return; 794 } 795 mSession.setSurface(surface); 796 } 797 798 private void dispatchSurfaceChanged(int format, int width, int height) { 799 if (mSession == null) { 800 return; 801 } 802 mSession.dispatchSurfaceChanged(format, width, height); 803 } 804 805 private void createSessionOverlayView() { 806 if (mSession == null || !isAttachedToWindow() 807 || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) { 808 return; 809 } 810 mOverlayViewFrame = getViewFrameOnScreen(); 811 mSession.createOverlayView(this, mOverlayViewFrame); 812 mOverlayViewCreated = true; 813 } 814 815 private void removeSessionOverlayView() { 816 if (mSession == null || !mOverlayViewCreated) { 817 return; 818 } 819 mSession.removeOverlayView(); 820 mOverlayViewCreated = false; 821 mOverlayViewFrame = null; 822 } 823 824 private void relayoutSessionOverlayView() { 825 if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated 826 || mWindowZOrder != ZORDER_MEDIA) { 827 return; 828 } 829 Rect viewFrame = getViewFrameOnScreen(); 830 if (viewFrame.equals(mOverlayViewFrame)) { 831 return; 832 } 833 mSession.relayoutOverlayView(viewFrame); 834 mOverlayViewFrame = viewFrame; 835 } 836 837 private Rect getViewFrameOnScreen() { 838 int[] location = new int[2]; 839 getLocationOnScreen(location); 840 return new Rect(location[0], location[1], 841 location[0] + getWidth(), location[1] + getHeight()); 842 } 843 844 /** 845 * Callback used to receive time shift position changes. 846 */ 847 public abstract static class TimeShiftPositionCallback { 848 849 /** 850 * This is called when the start playback position is changed. 851 * 852 * <p>The start playback position of the time shifted program can be adjusted by the TV 853 * input when it cannot retain the whole recorded program due to some reason (e.g. 854 * limitation on storage space). The application should not allow the user to seek to a 855 * position earlier than the start position. 856 * 857 * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time, 858 * which is intended to avoid calling this method unnecessarily around program boundaries. 859 * 860 * @param inputId The ID of the TV input bound to this view. 861 * @param timeMs The start playback position of the time shifted program, in milliseconds 862 * since the epoch. 863 */ 864 public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { 865 } 866 867 /** 868 * This is called when the current playback position is changed. 869 * 870 * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time, 871 * which is intended to avoid calling this method unnecessarily around program boundaries. 872 * 873 * @param inputId The ID of the TV input bound to this view. 874 * @param timeMs The current playback position of the time shifted program, in milliseconds 875 * since the epoch. 876 */ 877 public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { 878 } 879 } 880 881 /** 882 * Callback used to receive various status updates on the {@link TvView}. 883 */ 884 public abstract static class TvInputCallback { 885 886 /** 887 * This is invoked when an error occurred while establishing a connection to the underlying 888 * TV input. 889 * 890 * @param inputId The ID of the TV input bound to this view. 891 */ 892 public void onConnectionFailed(String inputId) { 893 } 894 895 /** 896 * This is invoked when the existing connection to the underlying TV input is lost. 897 * 898 * @param inputId The ID of the TV input bound to this view. 899 */ 900 public void onDisconnected(String inputId) { 901 } 902 903 /** 904 * This is invoked when the channel of this TvView is changed by the underlying TV input 905 * without any {@link TvView#tune} request. 906 * 907 * @param inputId The ID of the TV input bound to this view. 908 * @param channelUri The URI of a channel. 909 */ 910 public void onChannelRetuned(String inputId, Uri channelUri) { 911 } 912 913 /** 914 * This is called when the track information has been changed. 915 * 916 * @param inputId The ID of the TV input bound to this view. 917 * @param tracks A list which includes track information. 918 */ 919 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 920 } 921 922 /** 923 * This is called when there is a change on the selected tracks. 924 * 925 * @param inputId The ID of the TV input bound to this view. 926 * @param type The type of the track selected. The type can be 927 * {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or 928 * {@link TvTrackInfo#TYPE_SUBTITLE}. 929 * @param trackId The ID of the track selected. 930 */ 931 public void onTrackSelected(String inputId, int type, String trackId) { 932 } 933 934 /** 935 * This is invoked when the video size has been changed. It is also called when the first 936 * time video size information becomes available after this view is tuned to a specific 937 * channel. 938 * 939 * @param inputId The ID of the TV input bound to this view. 940 * @param width The width of the video. 941 * @param height The height of the video. 942 */ 943 public void onVideoSizeChanged(String inputId, int width, int height) { 944 } 945 946 /** 947 * This is called when the video is available, so the TV input starts the playback. 948 * 949 * @param inputId The ID of the TV input bound to this view. 950 */ 951 public void onVideoAvailable(String inputId) { 952 } 953 954 /** 955 * This is called when the video is not available, so the TV input stops the playback. 956 * 957 * @param inputId The ID of the TV input bound to this view. 958 * @param reason The reason why the TV input stopped the playback: 959 * <ul> 960 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN} 961 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING} 962 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL} 963 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING} 964 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY} 965 * </ul> 966 */ 967 public void onVideoUnavailable( 968 String inputId, @TvInputManager.VideoUnavailableReason int reason) { 969 } 970 971 /** 972 * This is called when the current program content turns out to be allowed to watch since 973 * its content rating is not blocked by parental controls. 974 * 975 * @param inputId The ID of the TV input bound to this view. 976 */ 977 public void onContentAllowed(String inputId) { 978 } 979 980 /** 981 * This is called when the current program content turns out to be not allowed to watch 982 * since its content rating is blocked by parental controls. 983 * 984 * @param inputId The ID of the TV input bound to this view. 985 * @param rating The content rating of the blocked program. 986 */ 987 public void onContentBlocked(String inputId, TvContentRating rating) { 988 } 989 990 /** 991 * This is invoked when a custom event from the bound TV input is sent to this view. 992 * 993 * @param inputId The ID of the TV input bound to this view. 994 * @param eventType The type of the event. 995 * @param eventArgs Optional arguments of the event. 996 * @hide 997 */ 998 @SystemApi 999 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 1000 } 1001 1002 /** 1003 * This is called when the time shift status is changed. 1004 * 1005 * @param inputId The ID of the TV input bound to this view. 1006 * @param status The current time shift status. Should be one of the followings. 1007 * <ul> 1008 * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED} 1009 * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE} 1010 * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} 1011 * </ul> 1012 */ 1013 public void onTimeShiftStatusChanged( 1014 String inputId, @TvInputManager.TimeShiftStatus int status) { 1015 } 1016 } 1017 1018 /** 1019 * Interface definition for a callback to be invoked when the unhandled input event is received. 1020 */ 1021 public interface OnUnhandledInputEventListener { 1022 /** 1023 * Called when an input event was not handled by the bound TV input. 1024 * 1025 * <p>This is called asynchronously from where the event is dispatched. It gives the host 1026 * application a chance to handle the unhandled input events. 1027 * 1028 * @param event The input event. 1029 * @return If you handled the event, return {@code true}. If you want to allow the event to 1030 * be handled by the next receiver, return {@code false}. 1031 */ 1032 boolean onUnhandledInputEvent(InputEvent event); 1033 } 1034 1035 private class MySessionCallback extends SessionCallback { 1036 final String mInputId; 1037 Uri mChannelUri; 1038 Bundle mTuneParams; 1039 Uri mRecordedProgramUri; 1040 1041 MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) { 1042 mInputId = inputId; 1043 mChannelUri = channelUri; 1044 mTuneParams = tuneParams; 1045 } 1046 1047 MySessionCallback(String inputId, Uri recordedProgramUri) { 1048 mInputId = inputId; 1049 mRecordedProgramUri = recordedProgramUri; 1050 } 1051 1052 @Override 1053 public void onSessionCreated(Session session) { 1054 if (DEBUG) { 1055 Log.d(TAG, "onSessionCreated()"); 1056 } 1057 if (this != mSessionCallback) { 1058 Log.w(TAG, "onSessionCreated - session already created"); 1059 // This callback is obsolete. 1060 if (session != null) { 1061 session.release(); 1062 } 1063 return; 1064 } 1065 mSession = session; 1066 if (session != null) { 1067 // Sends the pending app private commands first. 1068 for (Pair<String, Bundle> command : mPendingAppPrivateCommands) { 1069 mSession.sendAppPrivateCommand(command.first, command.second); 1070 } 1071 mPendingAppPrivateCommands.clear(); 1072 1073 synchronized (sMainTvViewLock) { 1074 if (hasWindowFocus() && TvView.this == sMainTvView.get()) { 1075 mSession.setMain(); 1076 } 1077 } 1078 // mSurface may not be ready yet as soon as starting an application. 1079 // In the case, we don't send Session.setSurface(null) unnecessarily. 1080 // setSessionSurface will be called in surfaceCreated. 1081 if (mSurface != null) { 1082 setSessionSurface(mSurface); 1083 if (mSurfaceChanged) { 1084 dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); 1085 } 1086 } 1087 createSessionOverlayView(); 1088 if (mStreamVolume != null) { 1089 mSession.setStreamVolume(mStreamVolume); 1090 } 1091 if (mCaptionEnabled != null) { 1092 mSession.setCaptionEnabled(mCaptionEnabled); 1093 } 1094 if (mChannelUri != null) { 1095 mSession.tune(mChannelUri, mTuneParams); 1096 } else { 1097 mSession.timeShiftPlay(mRecordedProgramUri); 1098 } 1099 ensurePositionTracking(); 1100 } else { 1101 mSessionCallback = null; 1102 if (mCallback != null) { 1103 mCallback.onConnectionFailed(mInputId); 1104 } 1105 } 1106 } 1107 1108 @Override 1109 public void onSessionReleased(Session session) { 1110 if (DEBUG) { 1111 Log.d(TAG, "onSessionReleased()"); 1112 } 1113 if (this != mSessionCallback) { 1114 Log.w(TAG, "onSessionReleased - session not created"); 1115 return; 1116 } 1117 mOverlayViewCreated = false; 1118 mOverlayViewFrame = null; 1119 mSessionCallback = null; 1120 mSession = null; 1121 if (mCallback != null) { 1122 mCallback.onDisconnected(mInputId); 1123 } 1124 } 1125 1126 @Override 1127 public void onChannelRetuned(Session session, Uri channelUri) { 1128 if (DEBUG) { 1129 Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")"); 1130 } 1131 if (this != mSessionCallback) { 1132 Log.w(TAG, "onChannelRetuned - session not created"); 1133 return; 1134 } 1135 if (mCallback != null) { 1136 mCallback.onChannelRetuned(mInputId, channelUri); 1137 } 1138 } 1139 1140 @Override 1141 public void onTracksChanged(Session session, List<TvTrackInfo> tracks) { 1142 if (DEBUG) { 1143 Log.d(TAG, "onTracksChanged(" + tracks + ")"); 1144 } 1145 if (this != mSessionCallback) { 1146 Log.w(TAG, "onTracksChanged - session not created"); 1147 return; 1148 } 1149 if (mCallback != null) { 1150 mCallback.onTracksChanged(mInputId, tracks); 1151 } 1152 } 1153 1154 @Override 1155 public void onTrackSelected(Session session, int type, String trackId) { 1156 if (DEBUG) { 1157 Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")"); 1158 } 1159 if (this != mSessionCallback) { 1160 Log.w(TAG, "onTrackSelected - session not created"); 1161 return; 1162 } 1163 if (mCallback != null) { 1164 mCallback.onTrackSelected(mInputId, type, trackId); 1165 } 1166 } 1167 1168 @Override 1169 public void onVideoSizeChanged(Session session, int width, int height) { 1170 if (DEBUG) { 1171 Log.d(TAG, "onVideoSizeChanged()"); 1172 } 1173 if (this != mSessionCallback) { 1174 Log.w(TAG, "onVideoSizeChanged - session not created"); 1175 return; 1176 } 1177 if (mCallback != null) { 1178 mCallback.onVideoSizeChanged(mInputId, width, height); 1179 } 1180 } 1181 1182 @Override 1183 public void onVideoAvailable(Session session) { 1184 if (DEBUG) { 1185 Log.d(TAG, "onVideoAvailable()"); 1186 } 1187 if (this != mSessionCallback) { 1188 Log.w(TAG, "onVideoAvailable - session not created"); 1189 return; 1190 } 1191 if (mCallback != null) { 1192 mCallback.onVideoAvailable(mInputId); 1193 } 1194 } 1195 1196 @Override 1197 public void onVideoUnavailable(Session session, int reason) { 1198 if (DEBUG) { 1199 Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")"); 1200 } 1201 if (this != mSessionCallback) { 1202 Log.w(TAG, "onVideoUnavailable - session not created"); 1203 return; 1204 } 1205 if (mCallback != null) { 1206 mCallback.onVideoUnavailable(mInputId, reason); 1207 } 1208 } 1209 1210 @Override 1211 public void onContentAllowed(Session session) { 1212 if (DEBUG) { 1213 Log.d(TAG, "onContentAllowed()"); 1214 } 1215 if (this != mSessionCallback) { 1216 Log.w(TAG, "onContentAllowed - session not created"); 1217 return; 1218 } 1219 if (mCallback != null) { 1220 mCallback.onContentAllowed(mInputId); 1221 } 1222 } 1223 1224 @Override 1225 public void onContentBlocked(Session session, TvContentRating rating) { 1226 if (DEBUG) { 1227 Log.d(TAG, "onContentBlocked(rating=" + rating + ")"); 1228 } 1229 if (this != mSessionCallback) { 1230 Log.w(TAG, "onContentBlocked - session not created"); 1231 return; 1232 } 1233 if (mCallback != null) { 1234 mCallback.onContentBlocked(mInputId, rating); 1235 } 1236 } 1237 1238 @Override 1239 public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { 1240 if (DEBUG) { 1241 Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" 1242 + right + ", bottom=" + bottom + ",)"); 1243 } 1244 if (this != mSessionCallback) { 1245 Log.w(TAG, "onLayoutSurface - session not created"); 1246 return; 1247 } 1248 mSurfaceViewLeft = left; 1249 mSurfaceViewTop = top; 1250 mSurfaceViewRight = right; 1251 mSurfaceViewBottom = bottom; 1252 mUseRequestedSurfaceLayout = true; 1253 requestLayout(); 1254 } 1255 1256 @Override 1257 public void onSessionEvent(Session session, String eventType, Bundle eventArgs) { 1258 if (DEBUG) { 1259 Log.d(TAG, "onSessionEvent(" + eventType + ")"); 1260 } 1261 if (this != mSessionCallback) { 1262 Log.w(TAG, "onSessionEvent - session not created"); 1263 return; 1264 } 1265 if (mCallback != null) { 1266 mCallback.onEvent(mInputId, eventType, eventArgs); 1267 } 1268 } 1269 1270 @Override 1271 public void onTimeShiftStatusChanged(Session session, int status) { 1272 if (DEBUG) { 1273 Log.d(TAG, "onTimeShiftStatusChanged()"); 1274 } 1275 if (this != mSessionCallback) { 1276 Log.w(TAG, "onTimeShiftStatusChanged - session not created"); 1277 return; 1278 } 1279 if (mCallback != null) { 1280 mCallback.onTimeShiftStatusChanged(mInputId, status); 1281 } 1282 } 1283 1284 @Override 1285 public void onTimeShiftStartPositionChanged(Session session, long timeMs) { 1286 if (DEBUG) { 1287 Log.d(TAG, "onTimeShiftStartPositionChanged()"); 1288 } 1289 if (this != mSessionCallback) { 1290 Log.w(TAG, "onTimeShiftStartPositionChanged - session not created"); 1291 return; 1292 } 1293 if (mTimeShiftPositionCallback != null) { 1294 mTimeShiftPositionCallback.onTimeShiftStartPositionChanged(mInputId, timeMs); 1295 } 1296 } 1297 1298 @Override 1299 public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) { 1300 if (DEBUG) { 1301 Log.d(TAG, "onTimeShiftCurrentPositionChanged()"); 1302 } 1303 if (this != mSessionCallback) { 1304 Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created"); 1305 return; 1306 } 1307 if (mTimeShiftPositionCallback != null) { 1308 mTimeShiftPositionCallback.onTimeShiftCurrentPositionChanged(mInputId, timeMs); 1309 } 1310 } 1311 } 1312} 1313