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