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