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.SystemApi; 20import android.content.Context; 21import android.graphics.Canvas; 22import android.graphics.PorterDuff; 23import android.graphics.Rect; 24import android.graphics.Region; 25import android.media.tv.TvInputManager.Session; 26import android.media.tv.TvInputManager.Session.FinishedInputEventCallback; 27import android.media.tv.TvInputManager.SessionCallback; 28import android.net.Uri; 29import android.os.Bundle; 30import android.os.Handler; 31import android.text.TextUtils; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.InputEvent; 35import android.view.KeyEvent; 36import android.view.MotionEvent; 37import android.view.Surface; 38import android.view.SurfaceHolder; 39import android.view.SurfaceView; 40import android.view.View; 41import android.view.ViewGroup; 42import android.view.ViewRootImpl; 43 44import java.lang.ref.WeakReference; 45import java.util.List; 46 47/** 48 * Displays TV contents. The TvView class provides a high level interface for applications to show 49 * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of 50 * TV inputs available on the system can be obtained by calling 51 * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.) 52 * <p> 53 * Once the application supplies the URI for a specific TV channel to {@link #tune(String, Uri)} 54 * method, it takes care of underlying service binding (and unbinding if the current TvView is 55 * already bound to a service) and automatically allocates/deallocates resources needed. In addition 56 * to a few essential methods to control how the contents are presented, it also provides a way to 57 * dispatch input events to the connected TvInputService in order to enable custom key actions for 58 * the TV input. 59 * </p> 60 */ 61public class TvView extends ViewGroup { 62 private static final String TAG = "TvView"; 63 private static final boolean DEBUG = false; 64 65 private static final int ZORDER_MEDIA = 0; 66 private static final int ZORDER_MEDIA_OVERLAY = 1; 67 private static final int ZORDER_ON_TOP = 2; 68 69 private static final int CAPTION_DEFAULT = 0; 70 private static final int CAPTION_ENABLED = 1; 71 private static final int CAPTION_DISABLED = 2; 72 73 private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null); 74 75 private static final Object sMainTvViewLock = new Object(); 76 private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW; 77 78 private final Handler mHandler = new Handler(); 79 private Session mSession; 80 private SurfaceView mSurfaceView; 81 private Surface mSurface; 82 private boolean mOverlayViewCreated; 83 private Rect mOverlayViewFrame; 84 private final TvInputManager mTvInputManager; 85 private MySessionCallback mSessionCallback; 86 private TvInputCallback mCallback; 87 private OnUnhandledInputEventListener mOnUnhandledInputEventListener; 88 private boolean mHasStreamVolume; 89 private float mStreamVolume; 90 private int mCaptionEnabled; 91 private String mAppPrivateCommandAction; 92 private Bundle mAppPrivateCommandData; 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 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 any existing 177 * callbacks. 178 */ 179 public void setCallback(TvInputCallback callback) { 180 mCallback = callback; 181 } 182 183 /** 184 * Sets this as the main {@link TvView}. 185 * <p> 186 * 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 * </p><p> 193 * 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 {@link #setMain} is called for other 195 * {@link TvView}. 196 * </p> 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 * Sets the Z order of a window owning the surface of this TvView above the normal TvView 211 * but below an application. 212 * 213 * @see SurfaceView#setZOrderMediaOverlay 214 * @hide 215 */ 216 @SystemApi 217 public void setZOrderMediaOverlay(boolean isMediaOverlay) { 218 if (isMediaOverlay) { 219 mWindowZOrder = ZORDER_MEDIA_OVERLAY; 220 removeSessionOverlayView(); 221 } else { 222 mWindowZOrder = ZORDER_MEDIA; 223 createSessionOverlayView(); 224 } 225 if (mSurfaceView != null) { 226 // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 227 // from WindowLayoutParam as well as changes window type. 228 mSurfaceView.setZOrderOnTop(false); 229 mSurfaceView.setZOrderMediaOverlay(isMediaOverlay); 230 } 231 } 232 233 /** 234 * Sets the Z order of a window owning the surface of this TvView on top of an application. 235 * 236 * @see SurfaceView#setZOrderOnTop 237 * @hide 238 */ 239 @SystemApi 240 public void setZOrderOnTop(boolean onTop) { 241 if (onTop) { 242 mWindowZOrder = ZORDER_ON_TOP; 243 removeSessionOverlayView(); 244 } else { 245 mWindowZOrder = ZORDER_MEDIA; 246 createSessionOverlayView(); 247 } 248 if (mSurfaceView != null) { 249 mSurfaceView.setZOrderMediaOverlay(false); 250 mSurfaceView.setZOrderOnTop(onTop); 251 } 252 } 253 254 /** 255 * Sets the relative stream volume of this session to handle a change of audio focus. 256 * 257 * @param volume A volume value between 0.0f to 1.0f. 258 */ 259 public void setStreamVolume(float volume) { 260 if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")"); 261 mHasStreamVolume = true; 262 mStreamVolume = volume; 263 if (mSession == null) { 264 // Volume will be set once the connection has been made. 265 return; 266 } 267 mSession.setStreamVolume(volume); 268 } 269 270 /** 271 * Tunes to a given channel. 272 * 273 * @param inputId The ID of TV input which will play the given channel. 274 * @param channelUri The URI of a channel. 275 */ 276 public void tune(String inputId, Uri channelUri) { 277 tune(inputId, channelUri, null); 278 } 279 280 /** 281 * Tunes to a given channel. 282 * 283 * @param inputId The ID of TV input which will play the given channel. 284 * @param channelUri The URI of a channel. 285 * @param params Extra parameters which might be handled with the tune event. 286 * @hide 287 */ 288 @SystemApi 289 public void tune(String inputId, Uri channelUri, Bundle params) { 290 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 291 if (TextUtils.isEmpty(inputId)) { 292 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 293 } 294 synchronized (sMainTvViewLock) { 295 if (sMainTvView.get() == null) { 296 sMainTvView = new WeakReference<>(this); 297 } 298 } 299 if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) { 300 if (mSession != null) { 301 mSession.tune(channelUri, params); 302 } else { 303 // Session is not created yet. Replace the channel which will be set once the 304 // session is made. 305 mSessionCallback.mChannelUri = channelUri; 306 mSessionCallback.mTuneParams = params; 307 } 308 } else { 309 resetInternal(); 310 // When createSession() is called multiple times before the callback is called, 311 // only the callback of the last createSession() call will be actually called back. 312 // The previous callbacks will be ignored. For the logic, mSessionCallback 313 // is newly assigned for every createSession request and compared with 314 // MySessionCreateCallback.this. 315 mSessionCallback = new MySessionCallback(inputId, channelUri, params); 316 if (mTvInputManager != null) { 317 mTvInputManager.createSession(inputId, mSessionCallback, mHandler); 318 } 319 } 320 } 321 322 /** 323 * Resets this TvView. 324 * <p> 325 * This method is primarily used to un-tune the current TvView. 326 */ 327 public void reset() { 328 if (DEBUG) Log.d(TAG, "reset()"); 329 synchronized (sMainTvViewLock) { 330 if (this == sMainTvView.get()) { 331 sMainTvView = NULL_TV_VIEW; 332 } 333 } 334 resetInternal(); 335 } 336 337 private void resetInternal() { 338 if (mSession != null) { 339 release(); 340 resetSurfaceView(); 341 } 342 } 343 344 /** 345 * Requests to unblock TV content according to the given rating. 346 * <p> 347 * This notifies TV input that blocked content is now OK to play. 348 * </p> 349 * 350 * @param unblockedRating A TvContentRating to unblock. 351 * @see TvInputService.Session#notifyContentBlocked(TvContentRating) 352 * @hide 353 */ 354 @SystemApi 355 public void requestUnblockContent(TvContentRating unblockedRating) { 356 if (mSession != null) { 357 mSession.requestUnblockContent(unblockedRating); 358 } 359 } 360 361 /** 362 * Enables or disables the caption in this TvView. 363 * <p> 364 * Note that this method does not take any effect unless the current TvView is tuned. 365 * 366 * @param enabled {@code true} to enable, {@code false} to disable. 367 */ 368 public void setCaptionEnabled(boolean enabled) { 369 mCaptionEnabled = enabled ? CAPTION_ENABLED : CAPTION_DISABLED; 370 if (mSession != null) { 371 mSession.setCaptionEnabled(enabled); 372 } 373 } 374 375 /** 376 * Selects a track. 377 * 378 * @param type The type of the track to select. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 379 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 380 * @param trackId The ID of the track to select. {@code null} means to unselect the current 381 * track for a given type. 382 * @see #getTracks 383 * @see #getSelectedTrack 384 */ 385 public void selectTrack(int type, String trackId) { 386 if (mSession != null) { 387 mSession.selectTrack(type, trackId); 388 } 389 } 390 391 /** 392 * Returns the list of tracks. Returns {@code null} if the information is not available. 393 * 394 * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 395 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 396 * @see #selectTrack 397 * @see #getSelectedTrack 398 */ 399 public List<TvTrackInfo> getTracks(int type) { 400 if (mSession == null) { 401 return null; 402 } 403 return mSession.getTracks(type); 404 } 405 406 /** 407 * Returns the ID of the selected track for a given type. Returns {@code null} if the 408 * information is not available or the track is not selected. 409 * 410 * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO}, 411 * {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}. 412 * @see #selectTrack 413 * @see #getTracks 414 */ 415 public String getSelectedTrack(int type) { 416 if (mSession == null) { 417 return null; 418 } 419 return mSession.getSelectedTrack(type); 420 } 421 422 /** 423 * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle) 424 * TvInputService.Session.appPrivateCommand()} on the current TvView. 425 * 426 * @param action The name of the private command to send. This <em>must</em> be a scoped name, 427 * i.e. prefixed with a package name you own, so that different developers will not 428 * create conflicting commands. 429 * @param data An optional bundle to send with the command. 430 * @hide 431 */ 432 @SystemApi 433 public void sendAppPrivateCommand(String action, Bundle data) { 434 if (TextUtils.isEmpty(action)) { 435 throw new IllegalArgumentException("action cannot be null or an empty string"); 436 } 437 if (mSession != null) { 438 mSession.sendAppPrivateCommand(action, data); 439 } else { 440 Log.w(TAG, "sendAppPrivateCommand - session not created (action " + action + " cached)"); 441 if (mAppPrivateCommandAction != null) { 442 Log.w(TAG, "previous cached action " + action + " removed"); 443 } 444 mAppPrivateCommandAction = action; 445 mAppPrivateCommandData = data; 446 } 447 } 448 449 /** 450 * Dispatches an unhandled input event to the next receiver. 451 * <p> 452 * Except system keys, TvView always consumes input events in the normal flow. This is called 453 * asynchronously from where the event is dispatched. It gives the host application a chance to 454 * dispatch the unhandled input events. 455 * 456 * @param event The input event. 457 * @return {@code true} if the event was handled by the view, {@code false} otherwise. 458 */ 459 public boolean dispatchUnhandledInputEvent(InputEvent event) { 460 if (mOnUnhandledInputEventListener != null) { 461 if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { 462 return true; 463 } 464 } 465 return onUnhandledInputEvent(event); 466 } 467 468 /** 469 * Called when an unhandled input event also has not been handled by the user provided 470 * callback. This is the last chance to handle the unhandled input event in the TvView. 471 * 472 * @param event The input event. 473 * @return If you handled the event, return {@code true}. If you want to allow the event to be 474 * handled by the next receiver, return {@code false}. 475 */ 476 public boolean onUnhandledInputEvent(InputEvent event) { 477 return false; 478 } 479 480 /** 481 * Registers a callback to be invoked when an input event is not handled by the bound TV input. 482 * 483 * @param listener The callback to be invoked when the unhandled input event is received. 484 */ 485 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 486 mOnUnhandledInputEventListener = listener; 487 } 488 489 @Override 490 public boolean dispatchKeyEvent(KeyEvent event) { 491 if (super.dispatchKeyEvent(event)) { 492 return true; 493 } 494 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 495 if (mSession == null) { 496 return false; 497 } 498 InputEvent copiedEvent = event.copy(); 499 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 500 mHandler); 501 return ret != Session.DISPATCH_NOT_HANDLED; 502 } 503 504 @Override 505 public boolean dispatchTouchEvent(MotionEvent event) { 506 if (super.dispatchTouchEvent(event)) { 507 return true; 508 } 509 if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); 510 if (mSession == null) { 511 return false; 512 } 513 InputEvent copiedEvent = event.copy(); 514 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 515 mHandler); 516 return ret != Session.DISPATCH_NOT_HANDLED; 517 } 518 519 @Override 520 public boolean dispatchTrackballEvent(MotionEvent event) { 521 if (super.dispatchTrackballEvent(event)) { 522 return true; 523 } 524 if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); 525 if (mSession == null) { 526 return false; 527 } 528 InputEvent copiedEvent = event.copy(); 529 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 530 mHandler); 531 return ret != Session.DISPATCH_NOT_HANDLED; 532 } 533 534 @Override 535 public boolean dispatchGenericMotionEvent(MotionEvent event) { 536 if (super.dispatchGenericMotionEvent(event)) { 537 return true; 538 } 539 if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); 540 if (mSession == null) { 541 return false; 542 } 543 InputEvent copiedEvent = event.copy(); 544 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 545 mHandler); 546 return ret != Session.DISPATCH_NOT_HANDLED; 547 } 548 549 @Override 550 public void dispatchWindowFocusChanged(boolean hasFocus) { 551 super.dispatchWindowFocusChanged(hasFocus); 552 // Other app may have shown its own main TvView. 553 // Set main again to regain main session. 554 synchronized (sMainTvViewLock) { 555 if (hasFocus && this == sMainTvView.get() && mSession != null) { 556 mSession.setMain(); 557 } 558 } 559 } 560 561 @Override 562 protected void onAttachedToWindow() { 563 super.onAttachedToWindow(); 564 createSessionOverlayView(); 565 } 566 567 @Override 568 protected void onDetachedFromWindow() { 569 removeSessionOverlayView(); 570 super.onDetachedFromWindow(); 571 } 572 573 @Override 574 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 575 if (DEBUG) { 576 Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right 577 + ", bottom=" + bottom + ",)"); 578 } 579 if (mUseRequestedSurfaceLayout) { 580 mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, 581 mSurfaceViewBottom); 582 } else { 583 mSurfaceView.layout(0, 0, right - left, bottom - top); 584 } 585 } 586 587 @Override 588 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 589 mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); 590 int width = mSurfaceView.getMeasuredWidth(); 591 int height = mSurfaceView.getMeasuredHeight(); 592 int childState = mSurfaceView.getMeasuredState(); 593 setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), 594 resolveSizeAndState(height, heightMeasureSpec, 595 childState << MEASURED_HEIGHT_STATE_SHIFT)); 596 } 597 598 @Override 599 public boolean gatherTransparentRegion(Region region) { 600 if (mWindowZOrder != ZORDER_ON_TOP) { 601 if (region != null) { 602 int width = getWidth(); 603 int height = getHeight(); 604 if (width > 0 && height > 0) { 605 int location[] = new int[2]; 606 getLocationInWindow(location); 607 int left = location[0]; 608 int top = location[1]; 609 region.op(left, top, left + width, top + height, Region.Op.UNION); 610 } 611 } 612 } 613 return super.gatherTransparentRegion(region); 614 } 615 616 @Override 617 public void draw(Canvas canvas) { 618 if (mWindowZOrder != ZORDER_ON_TOP) { 619 // Punch a hole so that the underlying overlay view and surface can be shown. 620 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 621 } 622 super.draw(canvas); 623 } 624 625 @Override 626 protected void dispatchDraw(Canvas canvas) { 627 if (mWindowZOrder != ZORDER_ON_TOP) { 628 // Punch a hole so that the underlying overlay view and surface can be shown. 629 canvas.drawColor(0, PorterDuff.Mode.CLEAR); 630 } 631 super.dispatchDraw(canvas); 632 } 633 634 @Override 635 protected void onVisibilityChanged(View changedView, int visibility) { 636 super.onVisibilityChanged(changedView, visibility); 637 mSurfaceView.setVisibility(visibility); 638 if (visibility == View.VISIBLE) { 639 createSessionOverlayView(); 640 } else { 641 removeSessionOverlayView(); 642 } 643 } 644 645 private void resetSurfaceView() { 646 if (mSurfaceView != null) { 647 mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); 648 removeView(mSurfaceView); 649 } 650 mSurface = null; 651 mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { 652 @Override 653 protected void updateWindow(boolean force, boolean redrawNeeded) { 654 super.updateWindow(force, redrawNeeded); 655 relayoutSessionOverlayView(); 656 }}; 657 mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); 658 if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) { 659 mSurfaceView.setZOrderMediaOverlay(true); 660 } else if (mWindowZOrder == ZORDER_ON_TOP) { 661 mSurfaceView.setZOrderOnTop(true); 662 } 663 addView(mSurfaceView); 664 } 665 666 private void release() { 667 mAppPrivateCommandAction = null; 668 mAppPrivateCommandData = null; 669 670 setSessionSurface(null); 671 removeSessionOverlayView(); 672 mUseRequestedSurfaceLayout = false; 673 mSession.release(); 674 mSession = null; 675 mSessionCallback = null; 676 } 677 678 private void setSessionSurface(Surface surface) { 679 if (mSession == null) { 680 return; 681 } 682 mSession.setSurface(surface); 683 } 684 685 private void dispatchSurfaceChanged(int format, int width, int height) { 686 if (mSession == null) { 687 return; 688 } 689 mSession.dispatchSurfaceChanged(format, width, height); 690 } 691 692 private void createSessionOverlayView() { 693 if (mSession == null || !isAttachedToWindow() 694 || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) { 695 return; 696 } 697 mOverlayViewFrame = getViewFrameOnScreen(); 698 mSession.createOverlayView(this, mOverlayViewFrame); 699 mOverlayViewCreated = true; 700 } 701 702 private void removeSessionOverlayView() { 703 if (mSession == null || !mOverlayViewCreated) { 704 return; 705 } 706 mSession.removeOverlayView(); 707 mOverlayViewCreated = false; 708 mOverlayViewFrame = null; 709 } 710 711 private void relayoutSessionOverlayView() { 712 if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated 713 || mWindowZOrder != ZORDER_MEDIA) { 714 return; 715 } 716 Rect viewFrame = getViewFrameOnScreen(); 717 if (viewFrame.equals(mOverlayViewFrame)) { 718 return; 719 } 720 mSession.relayoutOverlayView(viewFrame); 721 mOverlayViewFrame = viewFrame; 722 } 723 724 private Rect getViewFrameOnScreen() { 725 int[] location = new int[2]; 726 getLocationOnScreen(location); 727 return new Rect(location[0], location[1], 728 location[0] + getWidth(), location[1] + getHeight()); 729 } 730 731 /** 732 * Callback used to receive various status updates on the {@link TvView}. 733 */ 734 public abstract static class TvInputCallback { 735 736 /** 737 * This is invoked when an error occurred while establishing a connection to the underlying 738 * TV input. 739 * 740 * @param inputId The ID of the TV input bound to this view. 741 */ 742 public void onConnectionFailed(String inputId) { 743 } 744 745 /** 746 * This is invoked when the existing connection to the underlying TV input is lost. 747 * 748 * @param inputId The ID of the TV input bound to this view. 749 */ 750 public void onDisconnected(String inputId) { 751 } 752 753 /** 754 * This is invoked when the channel of this TvView is changed by the underlying TV input 755 * without any {@link TvView#tune(String, Uri)} request. 756 * 757 * @param inputId The ID of the TV input bound to this view. 758 * @param channelUri The URI of a channel. 759 */ 760 public void onChannelRetuned(String inputId, Uri channelUri) { 761 } 762 763 /** 764 * This is called when the track information has been changed. 765 * 766 * @param inputId The ID of the TV input bound to this view. 767 * @param tracks A list which includes track information. 768 */ 769 public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { 770 } 771 772 /** 773 * This is called when there is a change on the selected tracks. 774 * 775 * @param inputId The ID of the TV input bound to this view. 776 * @param type The type of the track selected. The type can be 777 * {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or 778 * {@link TvTrackInfo#TYPE_SUBTITLE}. 779 * @param trackId The ID of the track selected. 780 */ 781 public void onTrackSelected(String inputId, int type, String trackId) { 782 } 783 784 /** 785 * This is invoked when the video size has been changed. It is also called when the first 786 * time video size information becomes available after this view is tuned to a specific 787 * channel. 788 * 789 * @param inputId The ID of the TV input bound to this view. 790 * @param width The width of the video. 791 * @param height The height of the video. 792 */ 793 public void onVideoSizeChanged(String inputId, int width, int height) { 794 } 795 796 /** 797 * This is called when the video is available, so the TV input starts the playback. 798 * 799 * @param inputId The ID of the TV input bound to this view. 800 */ 801 public void onVideoAvailable(String inputId) { 802 } 803 804 /** 805 * This is called when the video is not available, so the TV input stops the playback. 806 * 807 * @param inputId The ID of the TV input bound to this view. 808 * @param reason The reason why the TV input stopped the playback: 809 * <ul> 810 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN} 811 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING} 812 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL} 813 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING} 814 * </ul> 815 */ 816 public void onVideoUnavailable(String inputId, int reason) { 817 } 818 819 /** 820 * This is called when the current program content turns out to be allowed to watch since 821 * its content rating is not blocked by parental controls. 822 * 823 * @param inputId The ID of the TV input bound to this view. 824 */ 825 public void onContentAllowed(String inputId) { 826 } 827 828 /** 829 * This is called when the current program content turns out to be not allowed to watch 830 * since its content rating is blocked by parental controls. 831 * 832 * @param inputId The ID of the TV input bound to this view. 833 * @param rating The content rating of the blocked program. 834 */ 835 public void onContentBlocked(String inputId, TvContentRating rating) { 836 } 837 838 /** 839 * This is invoked when a custom event from the bound TV input is sent to this view. 840 * 841 * @param eventType The type of the event. 842 * @param eventArgs Optional arguments of the event. 843 * @hide 844 */ 845 @SystemApi 846 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 847 } 848 } 849 850 /** 851 * Interface definition for a callback to be invoked when the unhandled input event is received. 852 */ 853 public interface OnUnhandledInputEventListener { 854 /** 855 * Called when an input event was not handled by the bound TV input. 856 * <p> 857 * This is called asynchronously from where the event is dispatched. It gives the host 858 * application a chance to handle the unhandled input events. 859 * 860 * @param event The input event. 861 * @return If you handled the event, return {@code true}. If you want to allow the event to 862 * be handled by the next receiver, return {@code false}. 863 */ 864 boolean onUnhandledInputEvent(InputEvent event); 865 } 866 867 private class MySessionCallback extends SessionCallback { 868 final String mInputId; 869 Uri mChannelUri; 870 Bundle mTuneParams; 871 872 MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) { 873 mInputId = inputId; 874 mChannelUri = channelUri; 875 mTuneParams = tuneParams; 876 } 877 878 @Override 879 public void onSessionCreated(Session session) { 880 if (DEBUG) { 881 Log.d(TAG, "onSessionCreated()"); 882 } 883 if (this != mSessionCallback) { 884 Log.w(TAG, "onSessionCreated - session already created"); 885 // This callback is obsolete. 886 if (session != null) { 887 session.release(); 888 } 889 return; 890 } 891 mSession = session; 892 if (session != null) { 893 synchronized (sMainTvViewLock) { 894 if (hasWindowFocus() && TvView.this == sMainTvView.get()) { 895 mSession.setMain(); 896 } 897 } 898 // mSurface may not be ready yet as soon as starting an application. 899 // In the case, we don't send Session.setSurface(null) unnecessarily. 900 // setSessionSurface will be called in surfaceCreated. 901 if (mSurface != null) { 902 setSessionSurface(mSurface); 903 if (mSurfaceChanged) { 904 dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); 905 } 906 } 907 createSessionOverlayView(); 908 if (mCaptionEnabled != CAPTION_DEFAULT) { 909 mSession.setCaptionEnabled(mCaptionEnabled == CAPTION_ENABLED); 910 } 911 mSession.tune(mChannelUri, mTuneParams); 912 if (mHasStreamVolume) { 913 mSession.setStreamVolume(mStreamVolume); 914 } 915 if (mAppPrivateCommandAction != null) { 916 mSession.sendAppPrivateCommand( 917 mAppPrivateCommandAction, mAppPrivateCommandData); 918 mAppPrivateCommandAction = null; 919 mAppPrivateCommandData = null; 920 } 921 } else { 922 mSessionCallback = null; 923 if (mCallback != null) { 924 mCallback.onConnectionFailed(mInputId); 925 } 926 } 927 } 928 929 @Override 930 public void onSessionReleased(Session session) { 931 if (DEBUG) { 932 Log.d(TAG, "onSessionReleased()"); 933 } 934 if (this != mSessionCallback) { 935 Log.w(TAG, "onSessionReleased - session not created"); 936 return; 937 } 938 mOverlayViewCreated = false; 939 mOverlayViewFrame = null; 940 mSessionCallback = null; 941 mSession = null; 942 if (mCallback != null) { 943 mCallback.onDisconnected(mInputId); 944 } 945 } 946 947 @Override 948 public void onChannelRetuned(Session session, Uri channelUri) { 949 if (DEBUG) { 950 Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")"); 951 } 952 if (this != mSessionCallback) { 953 Log.w(TAG, "onChannelRetuned - session not created"); 954 return; 955 } 956 if (mCallback != null) { 957 mCallback.onChannelRetuned(mInputId, channelUri); 958 } 959 } 960 961 @Override 962 public void onTracksChanged(Session session, List<TvTrackInfo> tracks) { 963 if (DEBUG) { 964 Log.d(TAG, "onTracksChanged(" + tracks + ")"); 965 } 966 if (this != mSessionCallback) { 967 Log.w(TAG, "onTracksChanged - session not created"); 968 return; 969 } 970 if (mCallback != null) { 971 mCallback.onTracksChanged(mInputId, tracks); 972 } 973 } 974 975 @Override 976 public void onTrackSelected(Session session, int type, String trackId) { 977 if (DEBUG) { 978 Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")"); 979 } 980 if (this != mSessionCallback) { 981 Log.w(TAG, "onTrackSelected - session not created"); 982 return; 983 } 984 if (mCallback != null) { 985 mCallback.onTrackSelected(mInputId, type, trackId); 986 } 987 } 988 989 @Override 990 public void onVideoSizeChanged(Session session, int width, int height) { 991 if (DEBUG) { 992 Log.d(TAG, "onVideoSizeChanged()"); 993 } 994 if (this != mSessionCallback) { 995 Log.w(TAG, "onVideoSizeChanged - session not created"); 996 return; 997 } 998 if (mCallback != null) { 999 mCallback.onVideoSizeChanged(mInputId, width, height); 1000 } 1001 } 1002 1003 @Override 1004 public void onVideoAvailable(Session session) { 1005 if (DEBUG) { 1006 Log.d(TAG, "onVideoAvailable()"); 1007 } 1008 if (this != mSessionCallback) { 1009 Log.w(TAG, "onVideoAvailable - session not created"); 1010 return; 1011 } 1012 if (mCallback != null) { 1013 mCallback.onVideoAvailable(mInputId); 1014 } 1015 } 1016 1017 @Override 1018 public void onVideoUnavailable(Session session, int reason) { 1019 if (DEBUG) { 1020 Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")"); 1021 } 1022 if (this != mSessionCallback) { 1023 Log.w(TAG, "onVideoUnavailable - session not created"); 1024 return; 1025 } 1026 if (mCallback != null) { 1027 mCallback.onVideoUnavailable(mInputId, reason); 1028 } 1029 } 1030 1031 @Override 1032 public void onContentAllowed(Session session) { 1033 if (DEBUG) { 1034 Log.d(TAG, "onContentAllowed()"); 1035 } 1036 if (this != mSessionCallback) { 1037 Log.w(TAG, "onContentAllowed - session not created"); 1038 return; 1039 } 1040 if (mCallback != null) { 1041 mCallback.onContentAllowed(mInputId); 1042 } 1043 } 1044 1045 @Override 1046 public void onContentBlocked(Session session, TvContentRating rating) { 1047 if (DEBUG) { 1048 Log.d(TAG, "onContentBlocked(rating=" + rating + ")"); 1049 } 1050 if (this != mSessionCallback) { 1051 Log.w(TAG, "onContentBlocked - session not created"); 1052 return; 1053 } 1054 if (mCallback != null) { 1055 mCallback.onContentBlocked(mInputId, rating); 1056 } 1057 } 1058 1059 @Override 1060 public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { 1061 if (DEBUG) { 1062 Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" 1063 + right + ", bottom=" + bottom + ",)"); 1064 } 1065 if (this != mSessionCallback) { 1066 Log.w(TAG, "onLayoutSurface - session not created"); 1067 return; 1068 } 1069 mSurfaceViewLeft = left; 1070 mSurfaceViewTop = top; 1071 mSurfaceViewRight = right; 1072 mSurfaceViewBottom = bottom; 1073 mUseRequestedSurfaceLayout = true; 1074 requestLayout(); 1075 } 1076 1077 @Override 1078 public void onSessionEvent(Session session, String eventType, Bundle eventArgs) { 1079 if (DEBUG) { 1080 Log.d(TAG, "onSessionEvent(" + eventType + ")"); 1081 } 1082 if (this != mSessionCallback) { 1083 Log.w(TAG, "onSessionEvent - session not created"); 1084 return; 1085 } 1086 if (mCallback != null) { 1087 mCallback.onEvent(mInputId, eventType, eventArgs); 1088 } 1089 } 1090 } 1091} 1092