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