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