TvView.java revision 6057102dbb746593a7d59cf377c969b62e38c664
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.content.Context; 20import android.graphics.Rect; 21import android.media.tv.TvInputManager.Session; 22import android.media.tv.TvInputManager.Session.FinishedInputEventCallback; 23import android.media.tv.TvInputManager.SessionCallback; 24import android.net.Uri; 25import android.os.Bundle; 26import android.os.Handler; 27import android.text.TextUtils; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.InputEvent; 31import android.view.KeyEvent; 32import android.view.MotionEvent; 33import android.view.Surface; 34import android.view.SurfaceHolder; 35import android.view.SurfaceView; 36import android.view.View; 37import android.view.ViewGroup; 38import android.view.ViewRootImpl; 39 40import java.util.List; 41 42/** 43 * View playing TV 44 */ 45public class TvView extends ViewGroup { 46 private static final String TAG = "TvView"; 47 // STOPSHIP: Turn debugging off. 48 private static final boolean DEBUG = true; 49 50 /** 51 * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the requested TV 52 * input is busy and unable to handle the request. 53 */ 54 public static final int ERROR_BUSY = 0; 55 56 /** 57 * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the underlying TV 58 * input has been disconnected. 59 */ 60 public static final int ERROR_TV_INPUT_DISCONNECTED = 1; 61 62 private static final int VIDEO_SIZE_VALUE_UNKNOWN = 0; 63 64 private final Handler mHandler = new Handler(); 65 private TvInputManager.Session mSession; 66 private final SurfaceView mSurfaceView; 67 private Surface mSurface; 68 private boolean mOverlayViewCreated; 69 private Rect mOverlayViewFrame; 70 private final TvInputManager mTvInputManager; 71 private MySessionCallback mSessionCallback; 72 private TvInputListener mListener; 73 private OnUnhandledInputEventListener mOnUnhandledInputEventListener; 74 private boolean mHasStreamVolume; 75 private float mStreamVolume; 76 private int mVideoWidth = VIDEO_SIZE_VALUE_UNKNOWN; 77 private int mVideoHeight = VIDEO_SIZE_VALUE_UNKNOWN; 78 79 private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 80 @Override 81 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 82 Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width 83 + ", height=" + height + ")"); 84 if (holder.getSurface() == mSurface) { 85 return; 86 } 87 mSurface = holder.getSurface(); 88 setSessionSurface(mSurface); 89 } 90 91 @Override 92 public void surfaceCreated(SurfaceHolder holder) { 93 mSurface = holder.getSurface(); 94 setSessionSurface(mSurface); 95 } 96 97 @Override 98 public void surfaceDestroyed(SurfaceHolder holder) { 99 mSurface = null; 100 setSessionSurface(null); 101 } 102 }; 103 104 private final FinishedInputEventCallback mFinishedInputEventCallback = 105 new FinishedInputEventCallback() { 106 @Override 107 public void onFinishedInputEvent(Object token, boolean handled) { 108 if (DEBUG) { 109 Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); 110 } 111 if (handled) { 112 return; 113 } 114 // TODO: Re-order unhandled events. 115 InputEvent event = (InputEvent) token; 116 if (dispatchUnhandledInputEvent(event)) { 117 return; 118 } 119 ViewRootImpl viewRootImpl = getViewRootImpl(); 120 if (viewRootImpl != null) { 121 viewRootImpl.dispatchUnhandledInputEvent(event); 122 } 123 } 124 }; 125 126 public TvView(Context context) { 127 this(context, null, 0); 128 } 129 130 public TvView(Context context, AttributeSet attrs) { 131 this(context, attrs, 0); 132 } 133 134 public TvView(Context context, AttributeSet attrs, int defStyleAttr) { 135 super(context, attrs, defStyleAttr); 136 mSurfaceView = new SurfaceView(context, attrs, defStyleAttr) { 137 @Override 138 protected void updateWindow(boolean force, boolean redrawNeeded) { 139 super.updateWindow(force, redrawNeeded); 140 relayoutSessionOverlayView(); 141 }}; 142 mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); 143 addView(mSurfaceView); 144 mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE); 145 } 146 147 /** 148 * Sets a listener for events in this TvView. 149 * 150 * @param listener The listener to be called with events. A value of {@code null} removes any 151 * existing listener. 152 */ 153 public void setTvInputListener(TvInputListener listener) { 154 mListener = listener; 155 } 156 157 /** 158 * Sets the relative stream volume of this session to handle a change of audio focus. 159 * 160 * @param volume A volume value between 0.0f to 1.0f. 161 */ 162 public void setStreamVolume(float volume) { 163 if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")"); 164 mHasStreamVolume = true; 165 mStreamVolume = volume; 166 if (mSession == null) { 167 // Volume will be set once the connection has been made. 168 return; 169 } 170 mSession.setStreamVolume(volume); 171 } 172 173 /** 174 * Tunes to a given channel. 175 * 176 * @param inputId the id of TV input which will play the given channel. 177 * @param channelUri The URI of a channel. 178 */ 179 public void tune(String inputId, Uri channelUri) { 180 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 181 if (TextUtils.isEmpty(inputId)) { 182 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 183 } 184 if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) { 185 if (mSession != null) { 186 mSession.tune(channelUri); 187 } else { 188 // Session is not created yet. Replace the channel which will be set once the 189 // session is made. 190 mSessionCallback.mChannelUri = channelUri; 191 } 192 } else { 193 if (mSession != null) { 194 release(); 195 } 196 // When createSession() is called multiple times before the callback is called, 197 // only the callback of the last createSession() call will be actually called back. 198 // The previous callbacks will be ignored. For the logic, mSessionCallback 199 // is newly assigned for every createSession request and compared with 200 // MySessionCreateCallback.this. 201 mSessionCallback = new MySessionCallback(inputId, channelUri); 202 mTvInputManager.createSession(inputId, mSessionCallback, mHandler); 203 } 204 } 205 206 /** 207 * Resets this TvView. 208 * <p> 209 * This method is primarily used to un-tune the current TvView. 210 */ 211 public void reset() { 212 if (mSession != null) { 213 release(); 214 } 215 } 216 217 /** 218 * Enables or disables the caption in this TvView. 219 * <p> 220 * Note that this method does not take any effect unless the current TvView is tuned. 221 * 222 * @param enabled {@code true} to enable, {@code false} to disable. 223 */ 224 public void setCaptionEnabled(boolean enabled) { 225 if (mSession != null) { 226 mSession.setCaptionEnabled(enabled); 227 } 228 } 229 230 /** 231 * Select a track. 232 * <p> 233 * If it is called multiple times on the same type of track (ie. Video, Audio, Text), the track 234 * selected in previous will be unselected. Note that this method does not take any effect 235 * unless the current TvView is tuned. 236 * </p> 237 * 238 * @param track the track to be selected. 239 * @see #getTracks() 240 */ 241 public void selectTrack(TvTrackInfo track) { 242 if (mSession != null) { 243 mSession.selectTrack(track); 244 } 245 } 246 247 /** 248 * Unselect a track. 249 * <p> 250 * Note that this method does not take any effect unless the current TvView is tuned. 251 * 252 * @param track the track to be unselected. 253 * @see #getTracks() 254 */ 255 public void unselectTrack(TvTrackInfo track) { 256 if (mSession != null) { 257 mSession.unselectTrack(track); 258 } 259 } 260 261 /** 262 * Returns a list which includes of track information. May return {@code null} if the 263 * information is not available. 264 */ 265 public List<TvTrackInfo> getTracks() { 266 if (mSession == null) { 267 return null; 268 } 269 return mSession.getTracks(); 270 } 271 272 /** 273 * Dispatches an unhandled input event to the next receiver. 274 * <p> 275 * Except system keys, TvView always consumes input events in the normal flow. This is called 276 * asynchronously from where the event is dispatched. It gives the host application a chance to 277 * dispatch the unhandled input events. 278 * 279 * @param event The input event. 280 * @return {@code true} if the event was handled by the view, {@code false} otherwise. 281 */ 282 public boolean dispatchUnhandledInputEvent(InputEvent event) { 283 if (mOnUnhandledInputEventListener != null) { 284 if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { 285 return true; 286 } 287 } 288 return onUnhandledInputEvent(event); 289 } 290 291 /** 292 * Called when an unhandled input event was also not handled by the user provided callback. This 293 * is the last chance to handle the unhandled input event in the TvView. 294 * 295 * @param event The input event. 296 * @return If you handled the event, return {@code true}. If you want to allow the event to be 297 * handled by the next receiver, return {@code false}. 298 */ 299 public boolean onUnhandledInputEvent(InputEvent event) { 300 return false; 301 } 302 303 /** 304 * Registers a callback to be invoked when an input event was not handled by the bound TV input. 305 * 306 * @param listener The callback to invoke when the unhandled input event was received. 307 */ 308 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 309 mOnUnhandledInputEventListener = listener; 310 } 311 312 @Override 313 public boolean dispatchKeyEvent(KeyEvent event) { 314 if (super.dispatchKeyEvent(event)) { 315 return true; 316 } 317 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 318 if (mSession == null) { 319 return false; 320 } 321 InputEvent copiedEvent = event.copy(); 322 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 323 mHandler); 324 return ret != Session.DISPATCH_NOT_HANDLED; 325 } 326 327 @Override 328 public boolean dispatchTouchEvent(MotionEvent event) { 329 if (super.dispatchTouchEvent(event)) { 330 return true; 331 } 332 if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); 333 if (mSession == null) { 334 return false; 335 } 336 InputEvent copiedEvent = event.copy(); 337 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 338 mHandler); 339 return ret != Session.DISPATCH_NOT_HANDLED; 340 } 341 342 @Override 343 public boolean dispatchTrackballEvent(MotionEvent event) { 344 if (super.dispatchTrackballEvent(event)) { 345 return true; 346 } 347 if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); 348 if (mSession == null) { 349 return false; 350 } 351 InputEvent copiedEvent = event.copy(); 352 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 353 mHandler); 354 return ret != Session.DISPATCH_NOT_HANDLED; 355 } 356 357 @Override 358 public boolean dispatchGenericMotionEvent(MotionEvent event) { 359 if (super.dispatchGenericMotionEvent(event)) { 360 return true; 361 } 362 if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); 363 if (mSession == null) { 364 return false; 365 } 366 InputEvent copiedEvent = event.copy(); 367 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 368 mHandler); 369 return ret != Session.DISPATCH_NOT_HANDLED; 370 } 371 372 @Override 373 protected void onAttachedToWindow() { 374 super.onAttachedToWindow(); 375 createSessionOverlayView(); 376 } 377 378 @Override 379 protected void onDetachedFromWindow() { 380 removeSessionOverlayView(); 381 super.onDetachedFromWindow(); 382 } 383 384 @Override 385 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 386 mSurfaceView.layout(0, 0, right - left, bottom - top); 387 } 388 389 @Override 390 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 391 mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); 392 int width = mSurfaceView.getMeasuredWidth(); 393 int height = mSurfaceView.getMeasuredHeight(); 394 int childState = mSurfaceView.getMeasuredState(); 395 setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), 396 resolveSizeAndState(height, heightMeasureSpec, 397 childState << MEASURED_HEIGHT_STATE_SHIFT)); 398 } 399 400 @Override 401 public void setVisibility(int visibility) { 402 super.setVisibility(visibility); 403 mSurfaceView.setVisibility(visibility); 404 if (visibility == View.VISIBLE) { 405 createSessionOverlayView(); 406 } else { 407 removeSessionOverlayView(); 408 } 409 } 410 411 private void release() { 412 setSessionSurface(null); 413 removeSessionOverlayView(); 414 mSession.release(); 415 mSession = null; 416 mSessionCallback = null; 417 } 418 419 private void setSessionSurface(Surface surface) { 420 if (mSession == null) { 421 return; 422 } 423 mSession.setSurface(surface); 424 } 425 426 private void createSessionOverlayView() { 427 if (mSession == null || !isAttachedToWindow() 428 || mOverlayViewCreated) { 429 return; 430 } 431 mOverlayViewFrame = getViewFrameOnScreen(); 432 mSession.createOverlayView(this, mOverlayViewFrame); 433 mOverlayViewCreated = true; 434 } 435 436 private void removeSessionOverlayView() { 437 if (mSession == null || !mOverlayViewCreated) { 438 return; 439 } 440 mSession.removeOverlayView(); 441 mOverlayViewCreated = false; 442 mOverlayViewFrame = null; 443 } 444 445 private void relayoutSessionOverlayView() { 446 if (mSession == null || !isAttachedToWindow() 447 || !mOverlayViewCreated) { 448 return; 449 } 450 Rect viewFrame = getViewFrameOnScreen(); 451 if (viewFrame.equals(mOverlayViewFrame)) { 452 return; 453 } 454 mSession.relayoutOverlayView(viewFrame); 455 mOverlayViewFrame = viewFrame; 456 } 457 458 private Rect getViewFrameOnScreen() { 459 int[] location = new int[2]; 460 getLocationOnScreen(location); 461 return new Rect(location[0], location[1], 462 location[0] + getWidth(), location[1] + getHeight()); 463 } 464 465 private void updateVideoSize(List<TvTrackInfo> tracks) { 466 for (TvTrackInfo track : tracks) { 467 if (track.getBoolean(TvTrackInfo.KEY_IS_SELECTED) 468 && track.getInt(TvTrackInfo.KEY_TYPE) == TvTrackInfo.VALUE_TYPE_VIDEO) { 469 int width = track.getInt(TvTrackInfo.KEY_WIDTH); 470 int height = track.getInt(TvTrackInfo.KEY_HEIGHT); 471 if (width != mVideoWidth || height != mVideoHeight) { 472 mVideoWidth = width; 473 mVideoHeight = height; 474 if (mListener != null) { 475 mListener.onVideoSizeChanged(mSessionCallback.mInputId, width, height); 476 } 477 } 478 } 479 } 480 } 481 482 /** 483 * Interface used to receive various status updates on the {@link TvView}. 484 */ 485 public abstract static class TvInputListener { 486 487 /** 488 * This is invoked when an error occurred while handling requested operation. 489 * 490 * @param inputId The ID of the TV input bound to this view. 491 * @param errorCode The error code. For the details of error code, please see 492 * {@link TvView}. 493 */ 494 public void onError(String inputId, int errorCode) { 495 } 496 497 /** 498 * This is invoked when the view is tuned to a specific channel and starts decoding video 499 * stream from there. It is also called later when the video size is changed. 500 * 501 * @param inputId The ID of the TV input bound to this view. 502 * @param width The width of the video. 503 * @param height The height of the video. 504 */ 505 public void onVideoSizeChanged(String inputId, int width, int height) { 506 } 507 508 /** 509 * This is invoked when the channel of this TvView is changed by the underlying TV input 510 * with out any {@link TvView#tune(String, Uri)} request. 511 * 512 * @param inputId The ID of the TV input bound to this view. 513 * @param channelUri The URI of a channel. 514 */ 515 public void onChannelRetuned(String inputId, Uri channelUri) { 516 } 517 518 /** 519 * This is called when the track information has been changed. 520 * 521 * @param inputId The ID of the TV input bound to this view. 522 * @param tracks A list which includes track information. 523 */ 524 public void onTrackInfoChanged(String inputId, List<TvTrackInfo> tracks) { 525 } 526 527 /** 528 * This is called when the video is available, so the TV input starts the playback. 529 * 530 * @param inputId The ID of the TV input bound to this view. 531 */ 532 public void onVideoAvailable(String inputId) { 533 } 534 535 /** 536 * This is called when the video is not available, so the TV input stops the playback. 537 * 538 * @param inputId The ID of the TV input bound to this view. 539 * @param reason The reason why the TV input stopped the playback: 540 * <ul> 541 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN} 542 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNE} 543 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL} 544 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING} 545 * </ul> 546 */ 547 public void onVideoUnavailable(String inputId, int reason) { 548 } 549 550 /** 551 * This is called when the current program content is blocked by parental controls. 552 * 553 * @param inputId The ID of the TV input bound to this view. 554 * @param rating The content rating of the blocked program. 555 */ 556 public void onContentBlocked(String inputId, TvContentRating rating) { 557 } 558 559 /** 560 * This is invoked when a custom event from the bound TV input is sent to this view. 561 * 562 * @param eventType The type of the event. 563 * @param eventArgs Optional arguments of the event. 564 * @hide 565 */ 566 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 567 } 568 } 569 570 /** 571 * Interface definition for a callback to be invoked when the unhandled input event is received. 572 */ 573 public interface OnUnhandledInputEventListener { 574 /** 575 * Called when an input event was not handled by the bound TV input. 576 * <p> 577 * This is called asynchronously from where the event is dispatched. It gives the host 578 * application a chance to handle the unhandled input events. 579 * 580 * @param event The input event. 581 * @return If you handled the event, return {@code true}. If you want to allow the event to 582 * be handled by the next receiver, return {@code false}. 583 */ 584 boolean onUnhandledInputEvent(InputEvent event); 585 } 586 587 private class MySessionCallback extends SessionCallback { 588 final String mInputId; 589 Uri mChannelUri; 590 591 MySessionCallback(String inputId, Uri channelUri) { 592 mInputId = inputId; 593 mChannelUri = channelUri; 594 } 595 596 @Override 597 public void onSessionCreated(Session session) { 598 if (this != mSessionCallback) { 599 // This callback is obsolete. 600 if (session != null) { 601 session.release(); 602 } 603 return; 604 } 605 mSession = session; 606 if (session != null) { 607 // mSurface may not be ready yet as soon as starting an application. 608 // In the case, we don't send Session.setSurface(null) unnecessarily. 609 // setSessionSurface will be called in surfaceCreated. 610 if (mSurface != null) { 611 setSessionSurface(mSurface); 612 } 613 createSessionOverlayView(); 614 mSession.tune(mChannelUri); 615 if (mHasStreamVolume) { 616 mSession.setStreamVolume(mStreamVolume); 617 } 618 } else { 619 if (mListener != null) { 620 mListener.onError(mInputId, ERROR_BUSY); 621 } 622 } 623 } 624 625 @Override 626 public void onSessionReleased(Session session) { 627 if (this == mSessionCallback) { 628 mSessionCallback = null; 629 } 630 mSession = null; 631 if (mListener != null) { 632 mListener.onError(mInputId, ERROR_TV_INPUT_DISCONNECTED); 633 } 634 } 635 636 @Override 637 public void onChannelRetuned(Session session, Uri channelUri) { 638 if (DEBUG) { 639 Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")"); 640 } 641 if (mListener != null) { 642 mListener.onChannelRetuned(mInputId, channelUri); 643 } 644 } 645 646 @Override 647 public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) { 648 if (this != mSessionCallback) { 649 return; 650 } 651 if (DEBUG) { 652 Log.d(TAG, "onTrackInfoChanged()"); 653 } 654 updateVideoSize(tracks); 655 if (mListener != null) { 656 mListener.onTrackInfoChanged(mInputId, tracks); 657 } 658 } 659 660 @Override 661 public void onVideoAvailable(Session session) { 662 if (DEBUG) { 663 Log.d(TAG, "onVideoAvailable()"); 664 } 665 if (mListener != null) { 666 mListener.onVideoAvailable(mInputId); 667 } 668 } 669 670 @Override 671 public void onVideoUnavailable(Session session, int reason) { 672 if (DEBUG) { 673 Log.d(TAG, "onVideoUnavailable(" + reason + ")"); 674 } 675 if (mListener != null) { 676 mListener.onVideoUnavailable(mInputId, reason); 677 } 678 } 679 680 @Override 681 public void onContentBlocked(Session session, TvContentRating rating) { 682 if (DEBUG) { 683 Log.d(TAG, "onContentBlocked()"); 684 } 685 if (mListener != null) { 686 mListener.onContentBlocked(mInputId, rating); 687 } 688 } 689 690 @Override 691 public void onSessionEvent(TvInputManager.Session session, String eventType, 692 Bundle eventArgs) { 693 if (this != mSessionCallback) { 694 return; 695 } 696 if (mListener != null) { 697 mListener.onEvent(mInputId, eventType, eventArgs); 698 } 699 } 700 } 701} 702