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