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