TvInputManager.java revision 9bf671f8ee72b156f16fcf05a3d1c6e093ecba67
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.graphics.Rect; 20import android.net.Uri; 21import android.os.Bundle; 22import android.os.Handler; 23import android.os.IBinder; 24import android.os.Looper; 25import android.os.Message; 26import android.os.RemoteException; 27import android.util.ArrayMap; 28import android.util.Log; 29import android.util.Pools.Pool; 30import android.util.Pools.SimplePool; 31import android.util.SparseArray; 32import android.view.InputChannel; 33import android.view.InputEvent; 34import android.view.InputEventSender; 35import android.view.Surface; 36import android.view.View; 37 38import java.util.ArrayList; 39import java.util.Iterator; 40import java.util.LinkedList; 41import java.util.List; 42import java.util.Map; 43 44/** 45 * Central system API to the overall TV input framework (TIF) architecture, which arbitrates 46 * interaction between applications and the selected TV inputs. 47 */ 48public final class TvInputManager { 49 private static final String TAG = "TvInputManager"; 50 51 static final int VIDEO_UNAVAILABLE_REASON_START = 0; 52 static final int VIDEO_UNAVAILABLE_REASON_END = 3; 53 54 /** 55 * A generic reason. Video is not available due to an unspecified error. 56 */ 57 public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START; 58 /** 59 * Video is not available because the TV input is tuning to another channel. 60 */ 61 public static final int VIDEO_UNAVAILABLE_REASON_TUNE = 1; 62 /** 63 * Video is not available due to the weak TV signal. 64 */ 65 public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2; 66 /** 67 * Video is not available because the TV input stopped the playback temporarily to buffer more 68 * data. 69 */ 70 public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = VIDEO_UNAVAILABLE_REASON_END; 71 72 /** 73 * The TV input is connected. 74 * <p> 75 * State for {@link #getInputState} and {@link 76 * TvInputManager.TvInputListener#onInputStateChanged}. 77 * </p> 78 */ 79 public static final int INPUT_STATE_CONNECTED = 0; 80 /** 81 * The TV input is connected but in standby mode. It would take a while until it becomes 82 * fully ready. 83 * <p> 84 * State for {@link #getInputState} and {@link 85 * TvInputManager.TvInputListener#onInputStateChanged}. 86 * </p> 87 */ 88 public static final int INPUT_STATE_CONNECTED_STANDBY = 1; 89 /** 90 * The TV input is disconnected. 91 * <p> 92 * State for {@link #getInputState} and {@link 93 * TvInputManager.TvInputListener#onInputStateChanged}. 94 * </p> 95 */ 96 public static final int INPUT_STATE_DISCONNECTED = 2; 97 98 private final ITvInputManager mService; 99 100 private final Object mLock = new Object(); 101 102 // @GuardedBy(mLock) 103 private final List<TvInputListenerRecord> mTvInputListenerRecordsList = 104 new LinkedList<TvInputListenerRecord>(); 105 106 // A mapping from TV input ID to the state of corresponding input. 107 // @GuardedBy(mLock) 108 private final Map<String, Integer> mStateMap = new ArrayMap<String, Integer>(); 109 110 // A mapping from the sequence number of a session to its SessionCallbackRecord. 111 private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = 112 new SparseArray<SessionCallbackRecord>(); 113 114 // A sequence number for the next session to be created. Should be protected by a lock 115 // {@code mSessionCallbackRecordMap}. 116 private int mNextSeq; 117 118 private final ITvInputClient mClient; 119 120 private final ITvInputManagerCallback mCallback; 121 122 private final int mUserId; 123 124 /** 125 * Interface used to receive the created session. 126 * @hide 127 */ 128 public abstract static class SessionCallback { 129 /** 130 * This is called after {@link TvInputManager#createSession} has been processed. 131 * 132 * @param session A {@link TvInputManager.Session} instance created. This can be 133 * {@code null} if the creation request failed. 134 */ 135 public void onSessionCreated(Session session) { 136 } 137 138 /** 139 * This is called when {@link TvInputManager.Session} is released. 140 * This typically happens when the process hosting the session has crashed or been killed. 141 * 142 * @param session A {@link TvInputManager.Session} instance released. 143 */ 144 public void onSessionReleased(Session session) { 145 } 146 147 /** 148 * This is called when the channel of this session is changed by the underlying TV input 149 * with out any {@link TvInputManager.Session#tune(Uri)} request. 150 * 151 * @param session A {@link TvInputManager.Session} associated with this callback 152 * @param channelUri The URI of a channel. 153 */ 154 public void onChannelRetuned(Session session, Uri channelUri) { 155 } 156 157 /** 158 * This is called when the track information of the session has been changed. 159 * 160 * @param session A {@link TvInputManager.Session} associated with this callback 161 * @param tracks A list which includes track information. 162 */ 163 public void onTrackInfoChanged(Session session, List<TvTrackInfo> tracks) { 164 } 165 166 /** 167 * This is called when the video is available, so the TV input starts the playback. 168 * 169 * @param session A {@link TvInputManager.Session} associated with this callback 170 */ 171 public void onVideoAvailable(Session session) { 172 } 173 174 /** 175 * This is called when the video is not available, so the TV input stops the playback. 176 * 177 * @param session A {@link TvInputManager.Session} associated with this callback 178 * @param reason The reason why the TV input stopped the playback: 179 * <ul> 180 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN} 181 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNE} 182 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL} 183 * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING} 184 * </ul> 185 */ 186 public void onVideoUnavailable(Session session, int reason) { 187 } 188 189 /** 190 * This is called when the current program content turns out to be allowed to watch since 191 * its content rating is not blocked by parental controls. 192 * 193 * @param session A {@link TvInputManager.Session} associated with this callback 194 */ 195 public void onContentAllowed(Session session) { 196 } 197 198 /** 199 * This is called when the current program content turns out to be not allowed to watch 200 * since its content rating is blocked by parental controls. 201 * 202 * @param session A {@link TvInputManager.Session} associated with this callback 203 * @param rating The content ration of the blocked program. 204 */ 205 public void onContentBlocked(Session session, TvContentRating rating) { 206 } 207 208 /** 209 * This is called when a custom event has been sent from this session. 210 * 211 * @param session A {@link TvInputManager.Session} associated with this callback 212 * @param eventType The type of the event. 213 * @param eventArgs Optional arguments of the event. 214 * @hide 215 */ 216 public void onSessionEvent(Session session, String eventType, Bundle eventArgs) { 217 } 218 } 219 220 private static final class SessionCallbackRecord { 221 private final SessionCallback mSessionCallback; 222 private final Handler mHandler; 223 private Session mSession; 224 225 public SessionCallbackRecord(SessionCallback sessionCallback, 226 Handler handler) { 227 mSessionCallback = sessionCallback; 228 mHandler = handler; 229 } 230 231 public void postSessionCreated(final Session session) { 232 mSession = session; 233 mHandler.post(new Runnable() { 234 @Override 235 public void run() { 236 mSessionCallback.onSessionCreated(session); 237 } 238 }); 239 } 240 241 public void postSessionReleased() { 242 mHandler.post(new Runnable() { 243 @Override 244 public void run() { 245 mSessionCallback.onSessionReleased(mSession); 246 } 247 }); 248 } 249 250 public void postChannelRetuned(final Uri channelUri) { 251 mHandler.post(new Runnable() { 252 @Override 253 public void run() { 254 mSessionCallback.onChannelRetuned(mSession, channelUri); 255 } 256 }); 257 } 258 259 public void postTrackInfoChanged(final List<TvTrackInfo> tracks) { 260 mHandler.post(new Runnable() { 261 @Override 262 public void run() { 263 mSession.setTracks(tracks); 264 mSessionCallback.onTrackInfoChanged(mSession, tracks); 265 } 266 }); 267 } 268 269 public void postVideoAvailable() { 270 mHandler.post(new Runnable() { 271 @Override 272 public void run() { 273 mSessionCallback.onVideoAvailable(mSession); 274 } 275 }); 276 } 277 278 public void postVideoUnavailable(final int reason) { 279 mHandler.post(new Runnable() { 280 @Override 281 public void run() { 282 mSessionCallback.onVideoUnavailable(mSession, reason); 283 } 284 }); 285 } 286 287 public void postContentAllowed() { 288 mHandler.post(new Runnable() { 289 @Override 290 public void run() { 291 mSessionCallback.onContentAllowed(mSession); 292 } 293 }); 294 } 295 296 public void postContentBlocked(final TvContentRating rating) { 297 mHandler.post(new Runnable() { 298 @Override 299 public void run() { 300 mSessionCallback.onContentBlocked(mSession, rating); 301 } 302 }); 303 } 304 305 public void postSessionEvent(final String eventType, final Bundle eventArgs) { 306 mHandler.post(new Runnable() { 307 @Override 308 public void run() { 309 mSessionCallback.onSessionEvent(mSession, eventType, eventArgs); 310 } 311 }); 312 } 313 } 314 315 /** 316 * Interface used to monitor status of the TV input. 317 */ 318 public abstract static class TvInputListener { 319 /** 320 * This is called when the state of a given TV input is changed. 321 * 322 * @param inputId The id of the TV input. 323 * @param state State of the TV input. The value is one of the following: 324 * <ul> 325 * <li>{@link TvInputManager#INPUT_STATE_CONNECTED} 326 * <li>{@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY} 327 * <li>{@link TvInputManager#INPUT_STATE_DISCONNECTED} 328 * </ul> 329 */ 330 public void onInputStateChanged(String inputId, int state) { 331 } 332 333 /** 334 * This is called when a TV input is added. 335 * 336 * @param inputId The id of the TV input. 337 */ 338 public void onInputAdded(String inputId) { 339 } 340 341 /** 342 * This is called when a TV input is removed. 343 * 344 * @param inputId The id of the TV input. 345 */ 346 public void onInputRemoved(String inputId) { 347 } 348 } 349 350 private static final class TvInputListenerRecord { 351 private final TvInputListener mListener; 352 private final Handler mHandler; 353 354 public TvInputListenerRecord(TvInputListener listener, Handler handler) { 355 mListener = listener; 356 mHandler = handler; 357 } 358 359 public TvInputListener getListener() { 360 return mListener; 361 } 362 363 public void postInputStateChanged(final String inputId, final int state) { 364 mHandler.post(new Runnable() { 365 @Override 366 public void run() { 367 mListener.onInputStateChanged(inputId, state); 368 } 369 }); 370 } 371 372 public void postInputAdded(final String inputId) { 373 mHandler.post(new Runnable() { 374 @Override 375 public void run() { 376 mListener.onInputAdded(inputId); 377 } 378 }); 379 } 380 381 public void postInputRemoved(final String inputId) { 382 mHandler.post(new Runnable() { 383 @Override 384 public void run() { 385 mListener.onInputRemoved(inputId); 386 } 387 }); 388 } 389 } 390 391 /** 392 * @hide 393 */ 394 public TvInputManager(ITvInputManager service, int userId) { 395 mService = service; 396 mUserId = userId; 397 mClient = new ITvInputClient.Stub() { 398 @Override 399 public void onSessionCreated(String inputId, IBinder token, InputChannel channel, 400 int seq) { 401 synchronized (mSessionCallbackRecordMap) { 402 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 403 if (record == null) { 404 Log.e(TAG, "Callback not found for " + token); 405 return; 406 } 407 Session session = null; 408 if (token != null) { 409 session = new Session(token, channel, mService, mUserId, seq, 410 mSessionCallbackRecordMap); 411 } 412 record.postSessionCreated(session); 413 } 414 } 415 416 @Override 417 public void onSessionReleased(int seq) { 418 synchronized (mSessionCallbackRecordMap) { 419 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 420 mSessionCallbackRecordMap.delete(seq); 421 if (record == null) { 422 Log.e(TAG, "Callback not found for seq:" + seq); 423 return; 424 } 425 record.mSession.releaseInternal(); 426 record.postSessionReleased(); 427 } 428 } 429 430 @Override 431 public void onChannelRetuned(Uri channelUri, int seq) { 432 synchronized (mSessionCallbackRecordMap) { 433 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 434 if (record == null) { 435 Log.e(TAG, "Callback not found for seq " + seq); 436 return; 437 } 438 record.postChannelRetuned(channelUri); 439 } 440 } 441 442 @Override 443 public void onTrackInfoChanged(List<TvTrackInfo> tracks, int seq) { 444 synchronized (mSessionCallbackRecordMap) { 445 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 446 if (record == null) { 447 Log.e(TAG, "Callback not found for seq " + seq); 448 return; 449 } 450 record.postTrackInfoChanged(tracks); 451 } 452 } 453 454 @Override 455 public void onVideoAvailable(int seq) { 456 synchronized (mSessionCallbackRecordMap) { 457 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 458 if (record == null) { 459 Log.e(TAG, "Callback not found for seq " + seq); 460 return; 461 } 462 record.postVideoAvailable(); 463 } 464 } 465 466 @Override 467 public void onVideoUnavailable(int reason, int seq) { 468 synchronized (mSessionCallbackRecordMap) { 469 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 470 if (record == null) { 471 Log.e(TAG, "Callback not found for seq " + seq); 472 return; 473 } 474 record.postVideoUnavailable(reason); 475 } 476 } 477 478 @Override 479 public void onContentAllowed(int seq) { 480 synchronized (mSessionCallbackRecordMap) { 481 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 482 if (record == null) { 483 Log.e(TAG, "Callback not found for seq " + seq); 484 return; 485 } 486 record.postContentAllowed(); 487 } 488 } 489 490 @Override 491 public void onContentBlocked(String rating, int seq) { 492 synchronized (mSessionCallbackRecordMap) { 493 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 494 if (record == null) { 495 Log.e(TAG, "Callback not found for seq " + seq); 496 return; 497 } 498 record.postContentBlocked(TvContentRating.unflattenFromString(rating)); 499 } 500 } 501 502 @Override 503 public void onSessionEvent(String eventType, Bundle eventArgs, int seq) { 504 synchronized (mSessionCallbackRecordMap) { 505 SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); 506 if (record == null) { 507 Log.e(TAG, "Callback not found for seq " + seq); 508 return; 509 } 510 record.postSessionEvent(eventType, eventArgs); 511 } 512 } 513 }; 514 mCallback = new ITvInputManagerCallback.Stub() { 515 @Override 516 public void onInputStateChanged(String inputId, int state) { 517 synchronized (mLock) { 518 mStateMap.put(inputId, state); 519 for (TvInputListenerRecord record : mTvInputListenerRecordsList) { 520 record.postInputStateChanged(inputId, state); 521 } 522 } 523 } 524 525 @Override 526 public void onInputAdded(String inputId) { 527 synchronized (mLock) { 528 mStateMap.put(inputId, INPUT_STATE_CONNECTED); 529 for (TvInputListenerRecord record : mTvInputListenerRecordsList) { 530 record.postInputAdded(inputId); 531 } 532 } 533 } 534 535 @Override 536 public void onInputRemoved(String inputId) { 537 synchronized (mLock) { 538 mStateMap.remove(inputId); 539 for (TvInputListenerRecord record : mTvInputListenerRecordsList) { 540 record.postInputRemoved(inputId); 541 } 542 } 543 } 544 }; 545 try { 546 mService.registerCallback(mCallback, mUserId); 547 } catch (RemoteException e) { 548 Log.e(TAG, "mService.registerCallback failed: " + e); 549 } 550 } 551 552 /** 553 * Returns the complete list of TV inputs on the system. 554 * 555 * @return List of {@link TvInputInfo} for each TV input that describes its meta information. 556 */ 557 public List<TvInputInfo> getTvInputList() { 558 try { 559 return mService.getTvInputList(mUserId); 560 } catch (RemoteException e) { 561 throw new RuntimeException(e); 562 } 563 } 564 565 /** 566 * Returns the {@link TvInputInfo} for a given TV input. 567 * 568 * @param inputId The ID of the TV input. 569 * @return the {@link TvInputInfo} for a given TV input. {@code null} if not found. 570 */ 571 public TvInputInfo getTvInputInfo(String inputId) { 572 try { 573 return mService.getTvInputInfo(inputId, mUserId); 574 } catch (RemoteException e) { 575 throw new RuntimeException(e); 576 } 577 } 578 579 /** 580 * Returns the state of a given TV input. It retuns one of the following: 581 * <ul> 582 * <li>{@link #INPUT_STATE_CONNECTED} 583 * <li>{@link #INPUT_STATE_CONNECTED_STANDBY} 584 * <li>{@link #INPUT_STATE_DISCONNECTED} 585 * </ul> 586 * 587 * @param inputId The id of the TV input. 588 * @throws IllegalArgumentException if the argument is {@code null} or if there is no 589 * {@link TvInputInfo} corresponding to {@code inputId}. 590 */ 591 public int getInputState(String inputId) { 592 if (inputId == null) { 593 throw new IllegalArgumentException("id cannot be null"); 594 } 595 synchronized (mLock) { 596 Integer state = mStateMap.get(inputId); 597 if (state == null) { 598 throw new IllegalArgumentException("Unrecognized input ID: " + inputId); 599 } 600 return state.intValue(); 601 } 602 } 603 604 /** 605 * Registers a {@link TvInputListener}. 606 * 607 * @param listener A listener used to monitor status of the TV inputs. 608 * @param handler A {@link Handler} that the status change will be delivered to. 609 * @throws IllegalArgumentException if any of the arguments is {@code null}. 610 */ 611 public void registerListener(TvInputListener listener, Handler handler) { 612 if (listener == null) { 613 throw new IllegalArgumentException("callback cannot be null"); 614 } 615 if (handler == null) { 616 throw new IllegalArgumentException("handler cannot be null"); 617 } 618 synchronized (mLock) { 619 mTvInputListenerRecordsList.add(new TvInputListenerRecord(listener, handler)); 620 } 621 } 622 623 /** 624 * Unregisters the existing {@link TvInputListener}. 625 * 626 * @param listener The existing listener to remove. 627 * @throws IllegalArgumentException if any of the arguments is {@code null}. 628 */ 629 public void unregisterListener(final TvInputListener listener) { 630 if (listener == null) { 631 throw new IllegalArgumentException("callback cannot be null"); 632 } 633 synchronized (mLock) { 634 for (Iterator<TvInputListenerRecord> it = mTvInputListenerRecordsList.iterator(); 635 it.hasNext(); ) { 636 TvInputListenerRecord record = it.next(); 637 if (record.getListener() == listener) { 638 it.remove(); 639 break; 640 } 641 } 642 } 643 } 644 645 /** 646 * Creates a {@link Session} for a given TV input. 647 * <p> 648 * The number of sessions that can be created at the same time is limited by the capability of 649 * the given TV input. 650 * </p> 651 * 652 * @param inputId The id of the TV input. 653 * @param callback A callback used to receive the created session. 654 * @param handler A {@link Handler} that the session creation will be delivered to. 655 * @throws IllegalArgumentException if any of the arguments is {@code null}. 656 * @hide 657 */ 658 public void createSession(String inputId, final SessionCallback callback, 659 Handler handler) { 660 if (inputId == null) { 661 throw new IllegalArgumentException("id cannot be null"); 662 } 663 if (callback == null) { 664 throw new IllegalArgumentException("callback cannot be null"); 665 } 666 if (handler == null) { 667 throw new IllegalArgumentException("handler cannot be null"); 668 } 669 SessionCallbackRecord record = new SessionCallbackRecord(callback, handler); 670 synchronized (mSessionCallbackRecordMap) { 671 int seq = mNextSeq++; 672 mSessionCallbackRecordMap.put(seq, record); 673 try { 674 mService.createSession(mClient, inputId, seq, mUserId); 675 } catch (RemoteException e) { 676 throw new RuntimeException(e); 677 } 678 } 679 } 680 681 /** 682 * The Session provides the per-session functionality of TV inputs. 683 * @hide 684 */ 685 public static final class Session { 686 static final int DISPATCH_IN_PROGRESS = -1; 687 static final int DISPATCH_NOT_HANDLED = 0; 688 static final int DISPATCH_HANDLED = 1; 689 690 private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; 691 692 private final ITvInputManager mService; 693 private final int mUserId; 694 private final int mSeq; 695 696 // For scheduling input event handling on the main thread. This also serves as a lock to 697 // protect pending input events and the input channel. 698 private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); 699 700 private final Pool<PendingEvent> mPendingEventPool = new SimplePool<PendingEvent>(20); 701 private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<PendingEvent>(20); 702 private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; 703 704 private IBinder mToken; 705 private TvInputEventSender mSender; 706 private InputChannel mChannel; 707 private List<TvTrackInfo> mTracks; 708 709 /** @hide */ 710 private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId, 711 int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { 712 mToken = token; 713 mChannel = channel; 714 mService = service; 715 mUserId = userId; 716 mSeq = seq; 717 mSessionCallbackRecordMap = sessionCallbackRecordMap; 718 } 719 720 /** 721 * Releases this session. 722 */ 723 public void release() { 724 if (mToken == null) { 725 Log.w(TAG, "The session has been already released"); 726 return; 727 } 728 try { 729 mService.releaseSession(mToken, mUserId); 730 } catch (RemoteException e) { 731 throw new RuntimeException(e); 732 } 733 734 releaseInternal(); 735 } 736 737 /** 738 * Sets the {@link android.view.Surface} for this session. 739 * 740 * @param surface A {@link android.view.Surface} used to render video. 741 * @hide 742 */ 743 public void setSurface(Surface surface) { 744 if (mToken == null) { 745 Log.w(TAG, "The session has been already released"); 746 return; 747 } 748 // surface can be null. 749 try { 750 mService.setSurface(mToken, surface, mUserId); 751 } catch (RemoteException e) { 752 throw new RuntimeException(e); 753 } 754 } 755 756 /** 757 * Notifies of any structural changes (format or size) of the {@link Surface} 758 * passed by {@link #setSurface}. 759 * 760 * @param format The new PixelFormat of the {@link Surface}. 761 * @param width The new width of the {@link Surface}. 762 * @param height The new height of the {@link Surface}. 763 * @hide 764 */ 765 public void dispatchSurfaceChanged(int format, int width, int height) { 766 if (mToken == null) { 767 Log.w(TAG, "The session has been already released"); 768 return; 769 } 770 try { 771 mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); 772 } catch (RemoteException e) { 773 throw new RuntimeException(e); 774 } 775 } 776 777 /** 778 * Sets the relative stream volume of this session to handle a change of audio focus. 779 * 780 * @param volume A volume value between 0.0f to 1.0f. 781 * @throws IllegalArgumentException if the volume value is out of range. 782 */ 783 public void setStreamVolume(float volume) { 784 if (mToken == null) { 785 Log.w(TAG, "The session has been already released"); 786 return; 787 } 788 try { 789 if (volume < 0.0f || volume > 1.0f) { 790 throw new IllegalArgumentException("volume should be between 0.0f and 1.0f"); 791 } 792 mService.setVolume(mToken, volume, mUserId); 793 } catch (RemoteException e) { 794 throw new RuntimeException(e); 795 } 796 } 797 798 /** 799 * Tunes to a given channel. 800 * 801 * @param channelUri The URI of a channel. 802 * @throws IllegalArgumentException if the argument is {@code null}. 803 */ 804 public void tune(Uri channelUri) { 805 if (channelUri == null) { 806 throw new IllegalArgumentException("channelUri cannot be null"); 807 } 808 if (mToken == null) { 809 Log.w(TAG, "The session has been already released"); 810 return; 811 } 812 mTracks = null; 813 try { 814 mService.tune(mToken, channelUri, mUserId); 815 } catch (RemoteException e) { 816 throw new RuntimeException(e); 817 } 818 } 819 820 /** 821 * Enables or disables the caption for this session. 822 * 823 * @param enabled {@code true} to enable, {@code false} to disable. 824 */ 825 public void setCaptionEnabled(boolean enabled) { 826 if (mToken == null) { 827 Log.w(TAG, "The session has been already released"); 828 return; 829 } 830 try { 831 mService.setCaptionEnabled(mToken, enabled, mUserId); 832 } catch (RemoteException e) { 833 throw new RuntimeException(e); 834 } 835 } 836 837 /** 838 * Select a track. 839 * 840 * @param track The track to be selected. 841 * @see #getTracks() 842 */ 843 public void selectTrack(TvTrackInfo track) { 844 if (track == null) { 845 throw new IllegalArgumentException("track cannot be null"); 846 } 847 if (mToken == null) { 848 Log.w(TAG, "The session has been already released"); 849 return; 850 } 851 try { 852 mService.selectTrack(mToken, track, mUserId); 853 } catch (RemoteException e) { 854 throw new RuntimeException(e); 855 } 856 } 857 858 /** 859 * Unselect a track. 860 * 861 * @param track The track to be selected. 862 * @see #getTracks() 863 */ 864 public void unselectTrack(TvTrackInfo track) { 865 if (track == null) { 866 throw new IllegalArgumentException("track cannot be null"); 867 } 868 if (mToken == null) { 869 Log.w(TAG, "The session has been already released"); 870 return; 871 } 872 try { 873 mService.unselectTrack(mToken, track, mUserId); 874 } catch (RemoteException e) { 875 throw new RuntimeException(e); 876 } 877 } 878 879 /** 880 * Returns a list which includes track information. May return {@code null} if the 881 * information is not available. 882 * @see #selectTrack(TvTrackInfo) 883 * @see #unselectTrack(TvTrackInfo) 884 */ 885 public List<TvTrackInfo> getTracks() { 886 if (mTracks == null) { 887 return null; 888 } 889 return new ArrayList<TvTrackInfo>(mTracks); 890 } 891 892 private void setTracks(List<TvTrackInfo> tracks) { 893 mTracks = tracks; 894 } 895 896 /** 897 * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView} 898 * should be called whenever the layout of its containing view is changed. 899 * {@link #removeOverlayView()} should be called to remove the overlay view. 900 * Since a session can have only one overlay view, this method should be called only once 901 * or it can be called again after calling {@link #removeOverlayView()}. 902 * 903 * @param view A view playing TV. 904 * @param frame A position of the overlay view. 905 * @throws IllegalArgumentException if any of the arguments is {@code null}. 906 * @throws IllegalStateException if {@code view} is not attached to a window. 907 */ 908 void createOverlayView(View view, Rect frame) { 909 if (view == null) { 910 throw new IllegalArgumentException("view cannot be null"); 911 } 912 if (frame == null) { 913 throw new IllegalArgumentException("frame cannot be null"); 914 } 915 if (view.getWindowToken() == null) { 916 throw new IllegalStateException("view must be attached to a window"); 917 } 918 if (mToken == null) { 919 Log.w(TAG, "The session has been already released"); 920 return; 921 } 922 try { 923 mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId); 924 } catch (RemoteException e) { 925 throw new RuntimeException(e); 926 } 927 } 928 929 /** 930 * Relayouts the current overlay view. 931 * 932 * @param frame A new position of the overlay view. 933 * @throws IllegalArgumentException if the arguments is {@code null}. 934 */ 935 void relayoutOverlayView(Rect frame) { 936 if (frame == null) { 937 throw new IllegalArgumentException("frame cannot be null"); 938 } 939 if (mToken == null) { 940 Log.w(TAG, "The session has been already released"); 941 return; 942 } 943 try { 944 mService.relayoutOverlayView(mToken, frame, mUserId); 945 } catch (RemoteException e) { 946 throw new RuntimeException(e); 947 } 948 } 949 950 /** 951 * Removes the current overlay view. 952 */ 953 void removeOverlayView() { 954 if (mToken == null) { 955 Log.w(TAG, "The session has been already released"); 956 return; 957 } 958 try { 959 mService.removeOverlayView(mToken, mUserId); 960 } catch (RemoteException e) { 961 throw new RuntimeException(e); 962 } 963 } 964 965 /** 966 * Requests to unblock content blocked by parental controls. 967 */ 968 void requestUnblockContent(TvContentRating unblockedRating) { 969 if (mToken == null) { 970 Log.w(TAG, "The session has been already released"); 971 return; 972 } 973 try { 974 mService.requestUnblockContent(mToken, unblockedRating.flattenToString(), mUserId); 975 } catch (RemoteException e) { 976 throw new RuntimeException(e); 977 } 978 } 979 980 /** 981 * Dispatches an input event to this session. 982 * 983 * @param event An {@link InputEvent} to dispatch. 984 * @param token A token used to identify the input event later in the callback. 985 * @param callback A callback used to receive the dispatch result. 986 * @param handler A {@link Handler} that the dispatch result will be delivered to. 987 * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns 988 * {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns 989 * {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will 990 * be invoked later. 991 * @throws IllegalArgumentException if any of the necessary arguments is {@code null}. 992 * @hide 993 */ 994 public int dispatchInputEvent(InputEvent event, Object token, 995 FinishedInputEventCallback callback, Handler handler) { 996 if (event == null) { 997 throw new IllegalArgumentException("event cannot be null"); 998 } 999 if (callback != null && handler == null) { 1000 throw new IllegalArgumentException("handler cannot be null"); 1001 } 1002 synchronized (mHandler) { 1003 if (mChannel == null) { 1004 return DISPATCH_NOT_HANDLED; 1005 } 1006 PendingEvent p = obtainPendingEventLocked(event, token, callback, handler); 1007 if (Looper.myLooper() == Looper.getMainLooper()) { 1008 // Already running on the main thread so we can send the event immediately. 1009 return sendInputEventOnMainLooperLocked(p); 1010 } 1011 1012 // Post the event to the main thread. 1013 Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p); 1014 msg.setAsynchronous(true); 1015 mHandler.sendMessage(msg); 1016 return DISPATCH_IN_PROGRESS; 1017 } 1018 } 1019 1020 /** 1021 * Callback that is invoked when an input event that was dispatched to this session has been 1022 * finished. 1023 * 1024 * @hide 1025 */ 1026 public interface FinishedInputEventCallback { 1027 /** 1028 * Called when the dispatched input event is finished. 1029 * 1030 * @param token A token passed to {@link #dispatchInputEvent}. 1031 * @param handled {@code true} if the dispatched input event was handled properly. 1032 * {@code false} otherwise. 1033 */ 1034 public void onFinishedInputEvent(Object token, boolean handled); 1035 } 1036 1037 // Must be called on the main looper 1038 private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { 1039 synchronized (mHandler) { 1040 int result = sendInputEventOnMainLooperLocked(p); 1041 if (result == DISPATCH_IN_PROGRESS) { 1042 return; 1043 } 1044 } 1045 1046 invokeFinishedInputEventCallback(p, false); 1047 } 1048 1049 private int sendInputEventOnMainLooperLocked(PendingEvent p) { 1050 if (mChannel != null) { 1051 if (mSender == null) { 1052 mSender = new TvInputEventSender(mChannel, mHandler.getLooper()); 1053 } 1054 1055 final InputEvent event = p.mEvent; 1056 final int seq = event.getSequenceNumber(); 1057 if (mSender.sendInputEvent(seq, event)) { 1058 mPendingEvents.put(seq, p); 1059 Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); 1060 msg.setAsynchronous(true); 1061 mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); 1062 return DISPATCH_IN_PROGRESS; 1063 } 1064 1065 Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" 1066 + event); 1067 } 1068 return DISPATCH_NOT_HANDLED; 1069 } 1070 1071 void finishedInputEvent(int seq, boolean handled, boolean timeout) { 1072 final PendingEvent p; 1073 synchronized (mHandler) { 1074 int index = mPendingEvents.indexOfKey(seq); 1075 if (index < 0) { 1076 return; // spurious, event already finished or timed out 1077 } 1078 1079 p = mPendingEvents.valueAt(index); 1080 mPendingEvents.removeAt(index); 1081 1082 if (timeout) { 1083 Log.w(TAG, "Timeout waiting for seesion to handle input event after " 1084 + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); 1085 } else { 1086 mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); 1087 } 1088 } 1089 1090 invokeFinishedInputEventCallback(p, handled); 1091 } 1092 1093 // Assumes the event has already been removed from the queue. 1094 void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { 1095 p.mHandled = handled; 1096 if (p.mHandler.getLooper().isCurrentThread()) { 1097 // Already running on the callback handler thread so we can send the callback 1098 // immediately. 1099 p.run(); 1100 } else { 1101 // Post the event to the callback handler thread. 1102 // In this case, the callback will be responsible for recycling the event. 1103 Message msg = Message.obtain(p.mHandler, p); 1104 msg.setAsynchronous(true); 1105 msg.sendToTarget(); 1106 } 1107 } 1108 1109 private void flushPendingEventsLocked() { 1110 mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); 1111 1112 final int count = mPendingEvents.size(); 1113 for (int i = 0; i < count; i++) { 1114 int seq = mPendingEvents.keyAt(i); 1115 Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); 1116 msg.setAsynchronous(true); 1117 msg.sendToTarget(); 1118 } 1119 } 1120 1121 private PendingEvent obtainPendingEventLocked(InputEvent event, Object token, 1122 FinishedInputEventCallback callback, Handler handler) { 1123 PendingEvent p = mPendingEventPool.acquire(); 1124 if (p == null) { 1125 p = new PendingEvent(); 1126 } 1127 p.mEvent = event; 1128 p.mToken = token; 1129 p.mCallback = callback; 1130 p.mHandler = handler; 1131 return p; 1132 } 1133 1134 private void recyclePendingEventLocked(PendingEvent p) { 1135 p.recycle(); 1136 mPendingEventPool.release(p); 1137 } 1138 1139 private void releaseInternal() { 1140 mToken = null; 1141 synchronized (mHandler) { 1142 if (mChannel != null) { 1143 if (mSender != null) { 1144 flushPendingEventsLocked(); 1145 mSender.dispose(); 1146 mSender = null; 1147 } 1148 mChannel.dispose(); 1149 mChannel = null; 1150 } 1151 } 1152 synchronized (mSessionCallbackRecordMap) { 1153 mSessionCallbackRecordMap.remove(mSeq); 1154 } 1155 } 1156 1157 private final class InputEventHandler extends Handler { 1158 public static final int MSG_SEND_INPUT_EVENT = 1; 1159 public static final int MSG_TIMEOUT_INPUT_EVENT = 2; 1160 public static final int MSG_FLUSH_INPUT_EVENT = 3; 1161 1162 InputEventHandler(Looper looper) { 1163 super(looper, null, true); 1164 } 1165 1166 @Override 1167 public void handleMessage(Message msg) { 1168 switch (msg.what) { 1169 case MSG_SEND_INPUT_EVENT: { 1170 sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); 1171 return; 1172 } 1173 case MSG_TIMEOUT_INPUT_EVENT: { 1174 finishedInputEvent(msg.arg1, false, true); 1175 return; 1176 } 1177 case MSG_FLUSH_INPUT_EVENT: { 1178 finishedInputEvent(msg.arg1, false, false); 1179 return; 1180 } 1181 } 1182 } 1183 } 1184 1185 private final class TvInputEventSender extends InputEventSender { 1186 public TvInputEventSender(InputChannel inputChannel, Looper looper) { 1187 super(inputChannel, looper); 1188 } 1189 1190 @Override 1191 public void onInputEventFinished(int seq, boolean handled) { 1192 finishedInputEvent(seq, handled, false); 1193 } 1194 } 1195 1196 private final class PendingEvent implements Runnable { 1197 public InputEvent mEvent; 1198 public Object mToken; 1199 public FinishedInputEventCallback mCallback; 1200 public Handler mHandler; 1201 public boolean mHandled; 1202 1203 public void recycle() { 1204 mEvent = null; 1205 mToken = null; 1206 mCallback = null; 1207 mHandler = null; 1208 mHandled = false; 1209 } 1210 1211 @Override 1212 public void run() { 1213 mCallback.onFinishedInputEvent(mToken, mHandled); 1214 1215 synchronized (mHandler) { 1216 recyclePendingEventLocked(this); 1217 } 1218 } 1219 } 1220 } 1221} 1222