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