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