TvView.java revision 336cdf20dd44ee93b5173be73e26e966a2609eb0
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.ViewRootImpl; 37 38/** 39 * View playing TV 40 */ 41public class TvView extends SurfaceView { 42 private static final String TAG = "TvView"; 43 // STOPSHIP: Turn debugging off. 44 private static final boolean DEBUG = true; 45 46 /** 47 * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the requested TV 48 * input is busy and unable to handle the request. 49 */ 50 public static final int ERROR_BUSY = 0; 51 52 /** 53 * Passed with {@link TvInputListener#onError(String, int)}. Indicates that the underlying TV 54 * input has been disconnected. 55 */ 56 public static final int ERROR_TV_INPUT_DISCONNECTED = 1; 57 58 private final Handler mHandler = new Handler(); 59 private TvInputManager.Session mSession; 60 private Surface mSurface; 61 private boolean mOverlayViewCreated; 62 private Rect mOverlayViewFrame; 63 private final TvInputManager mTvInputManager; 64 private MySessionCallback mSessionCallback; 65 private TvInputListener mListener; 66 private OnUnhandledInputEventListener mOnUnhandledInputEventListener; 67 private boolean mHasStreamVolume; 68 private float mStreamVolume; 69 70 private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { 71 @Override 72 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 73 Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width=" + width 74 + ", height=" + height + ")"); 75 if (holder.getSurface() == mSurface) { 76 return; 77 } 78 mSurface = holder.getSurface(); 79 setSessionSurface(mSurface); 80 } 81 82 @Override 83 public void surfaceCreated(SurfaceHolder holder) { 84 mSurface = holder.getSurface(); 85 setSessionSurface(mSurface); 86 } 87 88 @Override 89 public void surfaceDestroyed(SurfaceHolder holder) { 90 mSurface = null; 91 setSessionSurface(null); 92 } 93 }; 94 95 private final FinishedInputEventCallback mFinishedInputEventCallback = 96 new FinishedInputEventCallback() { 97 @Override 98 public void onFinishedInputEvent(Object token, boolean handled) { 99 if (DEBUG) { 100 Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")"); 101 } 102 if (handled) { 103 return; 104 } 105 // TODO: Re-order unhandled events. 106 InputEvent event = (InputEvent) token; 107 if (dispatchUnhandledInputEvent(event)) { 108 return; 109 } 110 ViewRootImpl viewRootImpl = getViewRootImpl(); 111 if (viewRootImpl != null) { 112 viewRootImpl.dispatchUnhandledInputEvent(event); 113 } 114 } 115 }; 116 117 public TvView(Context context) { 118 this(context, null, 0); 119 } 120 121 public TvView(Context context, AttributeSet attrs) { 122 this(context, attrs, 0); 123 } 124 125 public TvView(Context context, AttributeSet attrs, int defStyleAttr) { 126 super(context, attrs, defStyleAttr); 127 getHolder().addCallback(mSurfaceHolderCallback); 128 mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE); 129 } 130 131 /** 132 * Sets a listener for events in this TvView. 133 * 134 * @param listener The listener to be called with events. A value of {@code null} removes any 135 * existing listener. 136 */ 137 public void setTvInputListener(TvInputListener listener) { 138 mListener = listener; 139 } 140 141 /** 142 * Sets the relative stream volume of this session to handle a change of audio focus. 143 * 144 * @param volume A volume value between 0.0f to 1.0f. 145 */ 146 public void setStreamVolume(float volume) { 147 if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")"); 148 mHasStreamVolume = true; 149 mStreamVolume = volume; 150 if (mSession == null) { 151 // Volume will be set once the connection has been made. 152 return; 153 } 154 mSession.setStreamVolume(volume); 155 } 156 157 /** 158 * Tunes to a given channel. 159 * 160 * @param inputId the id of TV input which will play the given channel. 161 * @param channelUri The URI of a channel. 162 */ 163 public void tune(String inputId, Uri channelUri) { 164 if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")"); 165 if (TextUtils.isEmpty(inputId)) { 166 throw new IllegalArgumentException("inputId cannot be null or an empty string"); 167 } 168 if (mSessionCallback != null && mSessionCallback.mInputId.equals(inputId)) { 169 if (mSession != null) { 170 mSession.tune(channelUri); 171 } else { 172 // Session is not created yet. Replace the channel which will be set once the 173 // session is made. 174 mSessionCallback.mChannelUri = channelUri; 175 } 176 } else { 177 if (mSession != null) { 178 release(); 179 } 180 // When createSession() is called multiple times before the callback is called, 181 // only the callback of the last createSession() call will be actually called back. 182 // The previous callbacks will be ignored. For the logic, mSessionCallback 183 // is newly assigned for every createSession request and compared with 184 // MySessionCreateCallback.this. 185 mSessionCallback = new MySessionCallback(inputId, channelUri); 186 mTvInputManager.createSession(inputId, mSessionCallback, mHandler); 187 } 188 } 189 190 /** 191 * Resets this TvView. 192 * <p> 193 * This method is primarily used to un-tune the current TvView. 194 */ 195 public void reset() { 196 if (mSession != null) { 197 release(); 198 } 199 } 200 201 /** 202 * Dispatches an unhandled input event to the next receiver. 203 * <p> 204 * Except system keys, TvView always consumes input events in the normal flow. This is called 205 * asynchronously from where the event is dispatched. It gives the host application a chance to 206 * dispatch the unhandled input events. 207 * 208 * @param event The input event. 209 * @return {@code true} if the event was handled by the view, {@code false} otherwise. 210 */ 211 public boolean dispatchUnhandledInputEvent(InputEvent event) { 212 if (mOnUnhandledInputEventListener != null) { 213 if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) { 214 return true; 215 } 216 } 217 return onUnhandledInputEvent(event); 218 } 219 220 /** 221 * Called when an unhandled input event was also not handled by the user provided callback. This 222 * is the last chance to handle the unhandled input event in the TvView. 223 * 224 * @param event The input event. 225 * @return If you handled the event, return {@code true}. If you want to allow the event to be 226 * handled by the next receiver, return {@code false}. 227 */ 228 public boolean onUnhandledInputEvent(InputEvent event) { 229 return false; 230 } 231 232 /** 233 * Registers a callback to be invoked when an input event was not handled by the bound TV input. 234 * 235 * @param listener The callback to invoke when the unhandled input event was received. 236 */ 237 public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) { 238 mOnUnhandledInputEventListener = listener; 239 } 240 241 @Override 242 public boolean dispatchKeyEvent(KeyEvent event) { 243 if (super.dispatchKeyEvent(event)) { 244 return true; 245 } 246 if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")"); 247 if (mSession == null) { 248 return false; 249 } 250 InputEvent copiedEvent = event.copy(); 251 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 252 mHandler); 253 return ret != Session.DISPATCH_NOT_HANDLED; 254 } 255 256 @Override 257 public boolean dispatchTouchEvent(MotionEvent event) { 258 if (super.dispatchTouchEvent(event)) { 259 return true; 260 } 261 if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")"); 262 if (mSession == null) { 263 return false; 264 } 265 InputEvent copiedEvent = event.copy(); 266 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 267 mHandler); 268 return ret != Session.DISPATCH_NOT_HANDLED; 269 } 270 271 @Override 272 public boolean dispatchTrackballEvent(MotionEvent event) { 273 if (super.dispatchTrackballEvent(event)) { 274 return true; 275 } 276 if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")"); 277 if (mSession == null) { 278 return false; 279 } 280 InputEvent copiedEvent = event.copy(); 281 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 282 mHandler); 283 return ret != Session.DISPATCH_NOT_HANDLED; 284 } 285 286 @Override 287 public boolean dispatchGenericMotionEvent(MotionEvent event) { 288 if (super.dispatchGenericMotionEvent(event)) { 289 return true; 290 } 291 if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")"); 292 if (mSession == null) { 293 return false; 294 } 295 InputEvent copiedEvent = event.copy(); 296 int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback, 297 mHandler); 298 return ret != Session.DISPATCH_NOT_HANDLED; 299 } 300 301 @Override 302 protected void onAttachedToWindow() { 303 super.onAttachedToWindow(); 304 createSessionOverlayView(); 305 } 306 307 @Override 308 protected void onDetachedFromWindow() { 309 removeSessionOverlayView(); 310 super.onDetachedFromWindow(); 311 } 312 313 /** @hide */ 314 @Override 315 protected void updateWindow(boolean force, boolean redrawNeeded) { 316 super.updateWindow(force, redrawNeeded); 317 relayoutSessionOverlayView(); 318 } 319 320 private void release() { 321 setSessionSurface(null); 322 removeSessionOverlayView(); 323 mSession.release(); 324 mSession = null; 325 mSessionCallback = null; 326 } 327 328 private void setSessionSurface(Surface surface) { 329 if (mSession == null) { 330 return; 331 } 332 mSession.setSurface(surface); 333 } 334 335 private void createSessionOverlayView() { 336 if (mSession == null || !isAttachedToWindow() 337 || mOverlayViewCreated) { 338 return; 339 } 340 mOverlayViewFrame = getViewFrameOnScreen(); 341 mSession.createOverlayView(this, mOverlayViewFrame); 342 mOverlayViewCreated = true; 343 } 344 345 private void removeSessionOverlayView() { 346 if (mSession == null || !mOverlayViewCreated) { 347 return; 348 } 349 mSession.removeOverlayView(); 350 mOverlayViewCreated = false; 351 mOverlayViewFrame = null; 352 } 353 354 private void relayoutSessionOverlayView() { 355 if (mSession == null || !isAttachedToWindow() 356 || !mOverlayViewCreated) { 357 return; 358 } 359 Rect viewFrame = getViewFrameOnScreen(); 360 if (viewFrame.equals(mOverlayViewFrame)) { 361 return; 362 } 363 mSession.relayoutOverlayView(viewFrame); 364 mOverlayViewFrame = viewFrame; 365 } 366 367 private Rect getViewFrameOnScreen() { 368 int[] location = new int[2]; 369 getLocationOnScreen(location); 370 return new Rect(location[0], location[1], 371 location[0] + getWidth(), location[1] + getHeight()); 372 } 373 374 /** 375 * Interface used to receive various status updates on the {@link TvView}. 376 */ 377 public abstract static class TvInputListener { 378 379 /** 380 * This is invoked when an error occurred while handling requested operation. 381 * 382 * @param inputId The ID of the TV input bound to this view. 383 * @param errorCode The error code. For the details of error code, please see 384 * {@link TvView}. 385 */ 386 public void onError(String inputId, int errorCode) { 387 } 388 389 /** 390 * This is invoked when the view is tuned to a specific channel and starts decoding video 391 * stream from there. It is also called later when the video format is changed. 392 * 393 * @param inputId The ID of the TV input bound to this view. 394 * @param width The width of the video. 395 * @param height The height of the video. 396 * @param interlaced {@code true} if the video is interlaced, {@code false} if the video is 397 * progressive. 398 * @hide 399 */ 400 public void onVideoStreamChanged(String inputId, int width, int height, 401 boolean interlaced) { 402 } 403 404 /** 405 * This is invoked when the view is tuned to a specific channel and starts decoding audio 406 * stream from there. It is also called later when the audio format is changed. 407 * 408 * @param inputId The ID of the TV input bound to this view. 409 * @param channelCount The number of channels in the audio stream. 410 * @hide 411 */ 412 public void onAudioStreamChanged(String inputId, int channelCount) { 413 } 414 415 /** 416 * This is invoked when the view is tuned to a specific channel and starts decoding data 417 * stream that includes subtitle information from the channel. It is also called later when 418 * the information disappears or appears. 419 * 420 * @param inputId The ID of the TV input bound to this view. 421 * @param hasClosedCaption {@code true} if the stream contains closed caption, {@code false} 422 * otherwise. 423 * @hide 424 */ 425 public void onClosedCaptionStreamChanged(String inputId, boolean hasClosedCaption) { 426 } 427 428 /** 429 * This is invoked when a custom event from the bound TV input is sent to this view. 430 * 431 * @param eventType The type of the event. 432 * @param eventArgs Optional arguments of the event. 433 * @hide 434 */ 435 public void onEvent(String inputId, String eventType, Bundle eventArgs) { 436 } 437 } 438 439 /** 440 * Interface definition for a callback to be invoked when the unhandled input event is received. 441 */ 442 public interface OnUnhandledInputEventListener { 443 /** 444 * Called when an input event was not handled by the bound TV input. 445 * <p> 446 * This is called asynchronously from where the event is dispatched. It gives the host 447 * application a chance to handle the unhandled input events. 448 * 449 * @param event The input event. 450 * @return If you handled the event, return {@code true}. If you want to allow the event to 451 * be handled by the next receiver, return {@code false}. 452 */ 453 boolean onUnhandledInputEvent(InputEvent event); 454 } 455 456 private class MySessionCallback extends SessionCallback { 457 final String mInputId; 458 Uri mChannelUri; 459 460 MySessionCallback(String inputId, Uri channelUri) { 461 mInputId = inputId; 462 mChannelUri = channelUri; 463 } 464 465 @Override 466 public void onSessionCreated(Session session) { 467 if (this != mSessionCallback) { 468 // This callback is obsolete. 469 if (session != null) { 470 session.release(); 471 } 472 return; 473 } 474 mSession = session; 475 if (session != null) { 476 // mSurface may not be ready yet as soon as starting an application. 477 // In the case, we don't send Session.setSurface(null) unnecessarily. 478 // setSessionSurface will be called in surfaceCreated. 479 if (mSurface != null) { 480 setSessionSurface(mSurface); 481 } 482 createSessionOverlayView(); 483 mSession.tune(mChannelUri); 484 if (mHasStreamVolume) { 485 mSession.setStreamVolume(mStreamVolume); 486 } 487 } else { 488 if (mListener != null) { 489 mListener.onError(mInputId, ERROR_BUSY); 490 } 491 } 492 } 493 494 @Override 495 public void onSessionReleased(Session session) { 496 mSession = null; 497 mSessionCallback = null; 498 if (mListener != null) { 499 mListener.onError(mInputId, ERROR_TV_INPUT_DISCONNECTED); 500 } 501 } 502 503 @Override 504 public void onVideoStreamChanged(Session session, int width, int height, 505 boolean interlaced) { 506 if (DEBUG) { 507 Log.d(TAG, "onVideoSizeChanged(" + width + ", " + height + ")"); 508 } 509 if (mListener != null) { 510 mListener.onVideoStreamChanged(mInputId, width, height, interlaced); 511 } 512 } 513 514 @Override 515 public void onAudioStreamChanged(Session session, int channelCount) { 516 if (DEBUG) { 517 Log.d(TAG, "onAudioStreamChanged(" + channelCount + ")"); 518 } 519 if (mListener != null) { 520 mListener.onAudioStreamChanged(mInputId, channelCount); 521 } 522 } 523 524 @Override 525 public void onClosedCaptionStreamChanged(Session session, boolean hasClosedCaption) { 526 if (DEBUG) { 527 Log.d(TAG, "onClosedCaptionStreamChanged(" + hasClosedCaption + ")"); 528 } 529 if (mListener != null) { 530 mListener.onClosedCaptionStreamChanged(mInputId, hasClosedCaption); 531 } 532 } 533 534 @Override 535 public void onSessionEvent(TvInputManager.Session session, String eventType, 536 Bundle eventArgs) { 537 if (mListener != null) { 538 mListener.onEvent(mInputId, eventType, eventArgs); 539 } 540 } 541 } 542} 543