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