content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.media.tv;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.annotation.SystemApi;
  import android.annotation.SystemService;
  import android.content.Context;
  import android.content.Intent;
  import android.graphics.Rect;
  import android.media.PlaybackParams;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.ParcelFileDescriptor;
  import android.os.RemoteException;
  import android.text.TextUtils;
  import android.util.ArrayMap;
  import android.util.Log;
  import android.util.Pools.Pool;
  import android.util.Pools.SimplePool;
  import android.util.SparseArray;
  import android.view.InputChannel;
  import android.view.InputEvent;
  import android.view.InputEventSender;
  import android.view.KeyEvent;
  import android.view.Surface;
  import android.view.View;
  
  import com.android.internal.util.Preconditions;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Iterator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  
  /**
   * Central system API to the overall TV input framework (TIF) architecture, which arbitrates
   * interaction between applications and the selected TV inputs.
   *
   * <p>There are three primary parties involved in the TV input framework (TIF) architecture:
   *
   * <ul>
   * <li>The <strong>TV input manager</strong> as expressed by this class is the central point of the
   * system that manages interaction between all other parts. It is expressed as the client-side API
   * here which exists in each application context and communicates with a global system service that
   * manages the interaction across all processes.
   * <li>A <strong>TV input</strong> implemented by {@link TvInputService} represents an input source
   * of TV, which can be a pass-through input such as HDMI, or a tuner input which provides broadcast
   * TV programs. The system binds to the TV input per application’s request.
   * on implementing TV inputs.
   * <li><strong>Applications</strong> talk to the TV input manager to list TV inputs and check their
   * status. Once an application find the input to use, it uses {@link TvView} or
   * {@link TvRecordingClient} for further interaction such as watching and recording broadcast TV
   * programs.
   * </ul>
   */
  @SystemService(Context.TV_INPUT_SERVICE)
  public final class TvInputManager {
      private static final String TAG = "TvInputManager";
  
      static final int DVB_DEVICE_START = 0;
      static final int DVB_DEVICE_END = 2;
  
      /**
       * A demux device of DVB API for controlling the filters of DVB hardware/software.
       * @hide
       */
      public static final int DVB_DEVICE_DEMUX = DVB_DEVICE_START;
       /**
       * A DVR device of DVB API for reading transport streams.
       * @hide
       */
      public static final int DVB_DEVICE_DVR = 1;
      /**
       * A frontend device of DVB API for controlling the tuner and DVB demodulator hardware.
       * @hide
       */
      public static final int DVB_DEVICE_FRONTEND = DVB_DEVICE_END;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({VIDEO_UNAVAILABLE_REASON_UNKNOWN, VIDEO_UNAVAILABLE_REASON_TUNING,
              VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL, VIDEO_UNAVAILABLE_REASON_BUFFERING,
              VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY})
      public @interface VideoUnavailableReason {}
  
      static final int VIDEO_UNAVAILABLE_REASON_START = 0;
      static final int VIDEO_UNAVAILABLE_REASON_END = 5;
  
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable due to
       * an unspecified error.
       */
      public static final int VIDEO_UNAVAILABLE_REASON_UNKNOWN = VIDEO_UNAVAILABLE_REASON_START;
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
       * the corresponding TV input is in the middle of tuning to a new channel.
       */
      public static final int VIDEO_UNAVAILABLE_REASON_TUNING = 1;
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable due to
       * weak TV signal.
       */
      public static final int VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL = 2;
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
       * the corresponding TV input has stopped playback temporarily to buffer more data.
       */
      public static final int VIDEO_UNAVAILABLE_REASON_BUFFERING = 3;
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
       * the current TV program is audio-only.
       */
      public static final int VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY = 4;
      /**
       * Reason for {@link TvInputService.Session#notifyVideoUnavailable(int)} and
       * {@link TvView.TvInputCallback#onVideoUnavailable(String, int)}: Video is unavailable because
       * the source is not physically connected, for example the HDMI cable is not connected.
       * @hide
       */
      public static final int VIDEO_UNAVAILABLE_REASON_NOT_CONNECTED = VIDEO_UNAVAILABLE_REASON_END;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({TIME_SHIFT_STATUS_UNKNOWN, TIME_SHIFT_STATUS_UNSUPPORTED,
              TIME_SHIFT_STATUS_UNAVAILABLE, TIME_SHIFT_STATUS_AVAILABLE})
      public @interface TimeShiftStatus {}
  
      /**
       * Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
       * {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Unknown status. Also
       * the status prior to calling {@code notifyTimeShiftStatusChanged}.
       */
      public static final int TIME_SHIFT_STATUS_UNKNOWN = 0;
  
      /**
       * Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
       * {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: The current TV input
       * does not support time shifting.
       */
      public static final int TIME_SHIFT_STATUS_UNSUPPORTED = 1;
  
      /**
       * Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
       * {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Time shifting is
       * currently unavailable but might work again later.
       */
      public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2;
  
      /**
       * Status for {@link TvInputService.Session#notifyTimeShiftStatusChanged(int)} and
       * {@link TvView.TvInputCallback#onTimeShiftStatusChanged(String, int)}: Time shifting is
       * currently available. In this status, the application assumes it can pause/resume playback,
       * seek to a specified time position and set playback rate and audio mode.
       */
      public static final int TIME_SHIFT_STATUS_AVAILABLE = 3;
  
      /**
       * Value returned by {@link TvInputService.Session#onTimeShiftGetCurrentPosition()} and
       * {@link TvInputService.Session#onTimeShiftGetStartPosition()} when time shifting has not
       * yet started.
       */
      public static final long TIME_SHIFT_INVALID_TIME = Long.MIN_VALUE;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({RECORDING_ERROR_UNKNOWN, RECORDING_ERROR_INSUFFICIENT_SPACE,
              RECORDING_ERROR_RESOURCE_BUSY})
      public @interface RecordingError {}
  
      static final int RECORDING_ERROR_START = 0;
      static final int RECORDING_ERROR_END = 2;
  
      /**
       * Error for {@link TvInputService.RecordingSession#notifyError(int)} and
       * {@link TvRecordingClient.RecordingCallback#onError(int)}: The requested operation cannot be
       * completed due to a problem that does not fit under any other error codes, or the error code
       * for the problem is defined on the higher version than application's
       * <code>android:targetSdkVersion</code>.
       */
      public static final int RECORDING_ERROR_UNKNOWN = RECORDING_ERROR_START;
  
      /**
       * Error for {@link TvInputService.RecordingSession#notifyError(int)} and
       * {@link TvRecordingClient.RecordingCallback#onError(int)}: Recording cannot proceed due to
       * insufficient storage space.
       */
      public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 1;
  
      /**
       * Error for {@link TvInputService.RecordingSession#notifyError(int)} and
       * {@link TvRecordingClient.RecordingCallback#onError(int)}: Recording cannot proceed because
       * a required recording resource was not able to be allocated.
       */
      public static final int RECORDING_ERROR_RESOURCE_BUSY = RECORDING_ERROR_END;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({INPUT_STATE_CONNECTED, INPUT_STATE_CONNECTED_STANDBY, INPUT_STATE_DISCONNECTED})
      public @interface InputState {}
  
      /**
       * State for {@link #getInputState(String)} and
       * {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is connected.
       *
       * <p>This state indicates that a source device is connected to the input port and is in the
       * normal operation mode. It is mostly relevant to hardware inputs such as HDMI input.
       * Non-hardware inputs are considered connected all the time.
       */
      public static final int INPUT_STATE_CONNECTED = 0;
  
      /**
       * State for {@link #getInputState(String)} and
       * {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is connected but
       * in standby mode.
       *
       * <p>This state indicates that a source device is connected to the input port but is in standby
       * or low power mode. It is mostly relevant to hardware inputs such as HDMI input and Component
       * inputs.
       */
      public static final int INPUT_STATE_CONNECTED_STANDBY = 1;
  
      /**
       * State for {@link #getInputState(String)} and
       * {@link TvInputCallback#onInputStateChanged(String, int)}: The input source is disconnected.
       *
       * <p>This state indicates that a source device is disconnected from the input port. It is
       * mostly relevant to hardware inputs such as HDMI input.
       *
       */
      public static final int INPUT_STATE_DISCONNECTED = 2;
  
      /**
       * Broadcast intent action when the user blocked content ratings change. For use with the
       * {@link #isRatingBlocked}.
       */
      public static final String ACTION_BLOCKED_RATINGS_CHANGED =
              "android.media.tv.action.BLOCKED_RATINGS_CHANGED";
  
      /**
       * Broadcast intent action when the parental controls enabled state changes. For use with the
       * {@link #isParentalControlsEnabled}.
       */
      public static final String ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED =
              "android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED";
  
      /**
       * Broadcast intent action used to query available content rating systems.
       *
       * <p>The TV input manager service locates available content rating systems by querying
       * broadcast receivers that are registered for this action. An application can offer additional
       * content rating systems to the user by declaring a suitable broadcast receiver in its
       * manifest.
       *
       * <p>Here is an example broadcast receiver declaration that an application might include in its
       * AndroidManifest.xml to advertise custom content rating systems. The meta-data specifies a
       * resource that contains a description of each content rating system that is provided by the
       * application.
       *
       * <p><pre class="prettyprint">
       * {@literal
       * <receiver android:name=".TvInputReceiver">
       *     <intent-filter>
       *         <action android:name=
       *                 "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS" />
       *     </intent-filter>
       *     <meta-data
       *             android:name="android.media.tv.metadata.CONTENT_RATING_SYSTEMS"
       *             android:resource="@xml/tv_content_rating_systems" />
       * </receiver>}</pre>
       *
       * <p>In the above example, the <code>@xml/tv_content_rating_systems</code> resource refers to an
       * XML resource whose root element is <code>&lt;rating-system-definitions&gt;</code> that
       * contains zero or more <code>&lt;rating-system-definition&gt;</code> elements. Each <code>
       * &lt;rating-system-definition&gt;</code> element specifies the ratings, sub-ratings and rating
       * orders of a particular content rating system.
       *
       * @see TvContentRating
       */
      public static final String ACTION_QUERY_CONTENT_RATING_SYSTEMS =
              "android.media.tv.action.QUERY_CONTENT_RATING_SYSTEMS";
  
      /**
       * Content rating systems metadata associated with {@link #ACTION_QUERY_CONTENT_RATING_SYSTEMS}.
       *
       * <p>Specifies the resource ID of an XML resource that describes the content rating systems
       * that are provided by the application.
       */
      public static final String META_DATA_CONTENT_RATING_SYSTEMS =
              "android.media.tv.metadata.CONTENT_RATING_SYSTEMS";
  
      /**
       * Activity action to set up channel sources i.e.&nbsp;TV inputs of type
       * {@link TvInputInfo#TYPE_TUNER}. When invoked, the system will display an appropriate UI for
       * the user to initiate the individual setup flow provided by
       * {@link android.R.attr#setupActivity} of each TV input service.
       */
      public static final String ACTION_SETUP_INPUTS = "android.media.tv.action.SETUP_INPUTS";
  
      /**
       * Activity action to display the recording schedules. When invoked, the system will display an
       * appropriate UI to browse the schedules.
       */
      public static final String ACTION_VIEW_RECORDING_SCHEDULES =
              "android.media.tv.action.VIEW_RECORDING_SCHEDULES";
  
      private final ITvInputManager mService;
  
      private final Object mLock = new Object();
  
      // @GuardedBy("mLock")
      private final List<TvInputCallbackRecord> mCallbackRecords = new LinkedList<>();
  
      // A mapping from TV input ID to the state of corresponding input.
      // @GuardedBy("mLock")
      private final Map<String, Integer> mStateMap = new ArrayMap<>();
  
      // A mapping from the sequence number of a session to its SessionCallbackRecord.
      private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap =
              new SparseArray<>();
  
      // A sequence number for the next session to be created. Should be protected by a lock
      // {@code mSessionCallbackRecordMap}.
      private int mNextSeq;
  
      private final ITvInputClient mClient;
  
      private final int mUserId;
  
      /**
       * Interface used to receive the created session.
       * @hide
       */
      public abstract static class SessionCallback {
          /**
           * This is called after {@link TvInputManager#createSession} has been processed.
           *
           * @param session A {@link TvInputManager.Session} instance created. This can be
           *            {@code null} if the creation request failed.
           */
          public void onSessionCreated(@Nullable Session session) {
          }
  
          /**
           * This is called when {@link TvInputManager.Session} is released.
           * This typically happens when the process hosting the session has crashed or been killed.
           *
           * @param session A {@link TvInputManager.Session} instance released.
           */
          public void onSessionReleased(Session session) {
          }
  
          /**
           * This is called when the channel of this session is changed by the underlying TV input
           * without any {@link TvInputManager.Session#tune(Uri)} request.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param channelUri The URI of a channel.
           */
          public void onChannelRetuned(Session session, Uri channelUri) {
          }
  
          /**
           * This is called when the track information of the session has been changed.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param tracks A list which includes track information.
           */
          public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
          }
  
          /**
           * This is called when a track for a given type is selected.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param type The type of the selected track. The type can be
           *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
           *            {@link TvTrackInfo#TYPE_SUBTITLE}.
           * @param trackId The ID of the selected track. When {@code null} the currently selected
           *            track for a given type should be unselected.
           */
          public void onTrackSelected(Session session, int type, @Nullable String trackId) {
          }
  
          /**
           * This is invoked when the video size has been changed. It is also called when the first
           * time video size information becomes available after the session is tuned to a specific
           * channel.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param width The width of the video.
           * @param height The height of the video.
           */
          public void onVideoSizeChanged(Session session, int width, int height) {
          }
  
          /**
           * This is called when the video is available, so the TV input starts the playback.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           */
          public void onVideoAvailable(Session session) {
          }
  
          /**
           * This is called when the video is not available, so the TV input stops the playback.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param reason The reason why the TV input stopped the playback:
           * <ul>
           * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_UNKNOWN}
           * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_TUNING}
           * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL}
           * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_BUFFERING}
           * <li>{@link TvInputManager#VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY}
           * </ul>
           */
          public void onVideoUnavailable(Session session, int reason) {
          }
  
          /**
           * This is called when the current program content turns out to be allowed to watch since
           * its content rating is not blocked by parental controls.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           */
          public void onContentAllowed(Session session) {
          }
  
          /**
           * This is called when the current program content turns out to be not allowed to watch
           * since its content rating is blocked by parental controls.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param rating The content ration of the blocked program.
           */
          public void onContentBlocked(Session session, TvContentRating rating) {
          }
  
          /**
           * This is called when {@link TvInputService.Session#layoutSurface} is called to change the
           * layout of surface.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param left Left position.
           * @param top Top position.
           * @param right Right position.
           * @param bottom Bottom position.
           */
          public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
          }
  
          /**
           * This is called when a custom event has been sent from this session.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback
           * @param eventType The type of the event.
           * @param eventArgs Optional arguments of the event.
           */
          public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
          }
  
          /**
           * This is called when the time shift status is changed.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param status The current time shift status. Should be one of the followings.
           * <ul>
           * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED}
           * <li>{@link TvInputManager#TIME_SHIFT_STATUS_UNAVAILABLE}
           * <li>{@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE}
           * </ul>
           */
          public void onTimeShiftStatusChanged(Session session, int status) {
          }
  
          /**
           * This is called when the start position for time shifting has changed.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param timeMs The start position for time shifting, in milliseconds since the epoch.
           */
          public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
          }
  
          /**
           * This is called when the current position for time shifting is changed.
           *
           * @param session A {@link TvInputManager.Session} associated with this callback.
           * @param timeMs The current position for time shifting, in milliseconds since the epoch.
           */
          public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
          }
  
          // For the recording session only
          /**
           * This is called when the recording session has been tuned to the given channel and is
           * ready to start recording.
           *
           * @param channelUri The URI of a channel.
           */
          void onTuned(Session session, Uri channelUri) {
          }
  
          // For the recording session only
          /**
           * This is called when the current recording session has stopped recording and created a
           * new data entry in the {@link TvContract.RecordedPrograms} table that describes the newly
           * recorded program.
           *
           * @param recordedProgramUri The URI for the newly recorded program.
           **/
          void onRecordingStopped(Session session, Uri recordedProgramUri) {
          }
  
          // For the recording session only
          /**
           * This is called when an issue has occurred. It may be called at any time after the current
           * recording session is created until it is released.
           *
           * @param error The error code.
           */
          void onError(Session session, @TvInputManager.RecordingError int error) {
          }
      }
  
      private static final class SessionCallbackRecord {
          private final SessionCallback mSessionCallback;
          private final Handler mHandler;
          private Session mSession;
  
          SessionCallbackRecord(SessionCallback sessionCallback,
                  Handler handler) {
              mSessionCallback = sessionCallback;
              mHandler = handler;
          }
  
          void postSessionCreated(final Session session) {
              mSession = session;
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onSessionCreated(session);
                  }
              });
          }
  
          void postSessionReleased() {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onSessionReleased(mSession);
                  }
              });
          }
  
          void postChannelRetuned(final Uri channelUri) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onChannelRetuned(mSession, channelUri);
                  }
              });
          }
  
          void postTracksChanged(final List<TvTrackInfo> tracks) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTracksChanged(mSession, tracks);
                  }
              });
          }
  
          void postTrackSelected(final int type, final String trackId) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTrackSelected(mSession, type, trackId);
                  }
              });
          }
  
          void postVideoSizeChanged(final int width, final int height) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onVideoSizeChanged(mSession, width, height);
                  }
              });
          }
  
          void postVideoAvailable() {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onVideoAvailable(mSession);
                  }
              });
          }
  
          void postVideoUnavailable(final int reason) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onVideoUnavailable(mSession, reason);
                  }
              });
          }
  
          void postContentAllowed() {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onContentAllowed(mSession);
                  }
              });
          }
  
          void postContentBlocked(final TvContentRating rating) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onContentBlocked(mSession, rating);
                  }
              });
          }
  
          void postLayoutSurface(final int left, final int top, final int right,
                  final int bottom) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom);
                  }
              });
          }
  
          void postSessionEvent(final String eventType, final Bundle eventArgs) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onSessionEvent(mSession, eventType, eventArgs);
                  }
              });
          }
  
          void postTimeShiftStatusChanged(final int status) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTimeShiftStatusChanged(mSession, status);
                  }
              });
          }
  
          void postTimeShiftStartPositionChanged(final long timeMs) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTimeShiftStartPositionChanged(mSession, timeMs);
                  }
              });
          }
  
          void postTimeShiftCurrentPositionChanged(final long timeMs) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTimeShiftCurrentPositionChanged(mSession, timeMs);
                  }
              });
          }
  
          // For the recording session only
          void postTuned(final Uri channelUri) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onTuned(mSession, channelUri);
                  }
              });
          }
  
          // For the recording session only
          void postRecordingStopped(final Uri recordedProgramUri) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onRecordingStopped(mSession, recordedProgramUri);
                  }
              });
          }
  
          // For the recording session only
          void postError(final int error) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mSessionCallback.onError(mSession, error);
                  }
              });
          }
      }
  
      /**
       * Callback used to monitor status of the TV inputs.
       */
      public abstract static class TvInputCallback {
          /**
           * This is called when the state of a given TV input is changed.
           *
           * @param inputId The ID of the TV input.
           * @param state State of the TV input. The value is one of the following:
           * <ul>
           * <li>{@link TvInputManager#INPUT_STATE_CONNECTED}
           * <li>{@link TvInputManager#INPUT_STATE_CONNECTED_STANDBY}
           * <li>{@link TvInputManager#INPUT_STATE_DISCONNECTED}
           * </ul>
           */
          public void onInputStateChanged(String inputId, @InputState int state) {
          }
  
          /**
           * This is called when a TV input is added to the system.
           *
           * <p>Normally it happens when the user installs a new TV input package that implements
           * {@link TvInputService} interface.
           *
           * @param inputId The ID of the TV input.
           */
          public void onInputAdded(String inputId) {
          }
  
          /**
           * This is called when a TV input is removed from the system.
           *
           * <p>Normally it happens when the user uninstalls the previously installed TV input
           * package.
           *
           * @param inputId The ID of the TV input.
           */
          public void onInputRemoved(String inputId) {
          }
  
          /**
           * This is called when a TV input is updated on the system.
           *
           * <p>Normally it happens when a previously installed TV input package is re-installed or
           * the media on which a newer version of the package exists becomes available/unavailable.
           *
           * @param inputId The ID of the TV input.
           */
          public void onInputUpdated(String inputId) {
          }
  
          /**
           * This is called when the information about an existing TV input has been updated.
           *
           * <p>Because the system automatically creates a <code>TvInputInfo</code> object for each TV
           * input based on the information collected from the <code>AndroidManifest.xml</code>, this
           * method is only called back when such information has changed dynamically.
           *
           * @param inputInfo The <code>TvInputInfo</code> object that contains new information.
           */
          public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
          }
      }
  
      private static final class TvInputCallbackRecord {
          private final TvInputCallback mCallback;
          private final Handler mHandler;
  
          public TvInputCallbackRecord(TvInputCallback callback, Handler handler) {
              mCallback = callback;
              mHandler = handler;
          }
  
          public TvInputCallback getCallback() {
              return mCallback;
          }
  
          public void postInputAdded(final String inputId) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mCallback.onInputAdded(inputId);
                  }
              });
          }
  
          public void postInputRemoved(final String inputId) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mCallback.onInputRemoved(inputId);
                  }
              });
          }
  
          public void postInputUpdated(final String inputId) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mCallback.onInputUpdated(inputId);
                  }
              });
          }
  
          public void postInputStateChanged(final String inputId, final int state) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mCallback.onInputStateChanged(inputId, state);
                  }
              });
          }
  
          public void postTvInputInfoUpdated(final TvInputInfo inputInfo) {
              mHandler.post(new Runnable() {
                  @Override
                  public void run() {
                      mCallback.onTvInputInfoUpdated(inputInfo);
                  }
              });
          }
      }
  
      /**
       * Interface used to receive events from Hardware objects.
       *
       * @hide
       */
      @SystemApi
      public abstract static class HardwareCallback {
          /**
           * This is called when {@link Hardware} is no longer available for the client.
           */
          public abstract void onReleased();
  
          /**
           * This is called when the underlying {@link TvStreamConfig} has been changed.
           *
           * @param configs The new {@link TvStreamConfig}s.
           */
          public abstract void onStreamConfigChanged(TvStreamConfig[] configs);
      }
  
      /**
       * @hide
       */
      public TvInputManager(ITvInputManager service, int userId) {
          mService = service;
          mUserId = userId;
          mClient = new ITvInputClient.Stub() {
              @Override
              public void onSessionCreated(String inputId, IBinder token, InputChannel channel,
                      int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for " + token);
                          return;
                      }
                      Session session = null;
                      if (token != null) {
                          session = new Session(token, channel, mService, mUserId, seq,
                                  mSessionCallbackRecordMap);
                      } else {
                          mSessionCallbackRecordMap.delete(seq);
                      }
                      record.postSessionCreated(session);
                  }
              }
  
              @Override
              public void onSessionReleased(int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      mSessionCallbackRecordMap.delete(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq:" + seq);
                          return;
                      }
                      record.mSession.releaseInternal();
                      record.postSessionReleased();
                  }
              }
  
              @Override
              public void onChannelRetuned(Uri channelUri, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postChannelRetuned(channelUri);
                  }
              }
  
              @Override
              public void onTracksChanged(List<TvTrackInfo> tracks, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      if (record.mSession.updateTracks(tracks)) {
                          record.postTracksChanged(tracks);
                          postVideoSizeChangedIfNeededLocked(record);
                      }
                  }
              }
  
              @Override
              public void onTrackSelected(int type, String trackId, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      if (record.mSession.updateTrackSelection(type, trackId)) {
                          record.postTrackSelected(type, trackId);
                          postVideoSizeChangedIfNeededLocked(record);
                      }
                  }
              }
  
              private void postVideoSizeChangedIfNeededLocked(SessionCallbackRecord record) {
                  TvTrackInfo track = record.mSession.getVideoTrackToNotify();
                  if (track != null) {
                      record.postVideoSizeChanged(track.getVideoWidth(), track.getVideoHeight());
                  }
              }
  
              @Override
              public void onVideoAvailable(int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postVideoAvailable();
                  }
              }
  
              @Override
              public void onVideoUnavailable(int reason, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postVideoUnavailable(reason);
                  }
              }
  
              @Override
              public void onContentAllowed(int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postContentAllowed();
                  }
              }
  
              @Override
              public void onContentBlocked(String rating, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postContentBlocked(TvContentRating.unflattenFromString(rating));
                  }
              }
  
              @Override
              public void onLayoutSurface(int left, int top, int right, int bottom, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postLayoutSurface(left, top, right, bottom);
                  }
              }
  
              @Override
              public void onSessionEvent(String eventType, Bundle eventArgs, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postSessionEvent(eventType, eventArgs);
                  }
              }
  
              @Override
              public void onTimeShiftStatusChanged(int status, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postTimeShiftStatusChanged(status);
                  }
              }
  
              @Override
              public void onTimeShiftStartPositionChanged(long timeMs, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postTimeShiftStartPositionChanged(timeMs);
                  }
              }
  
              @Override
              public void onTimeShiftCurrentPositionChanged(long timeMs, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postTimeShiftCurrentPositionChanged(timeMs);
                  }
              }
  
              @Override
              public void onTuned(int seq, Uri channelUri) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postTuned(channelUri);
                  }
              }
  
              @Override
              public void onRecordingStopped(Uri recordedProgramUri, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postRecordingStopped(recordedProgramUri);
                  }
              }
  
              @Override
              public void onError(int error, int seq) {
                  synchronized (mSessionCallbackRecordMap) {
                      SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
                      if (record == null) {
                          Log.e(TAG, "Callback not found for seq " + seq);
                          return;
                      }
                      record.postError(error);
                  }
              }
          };
          ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() {
              @Override
              public void onInputAdded(String inputId) {
                  synchronized (mLock) {
                      mStateMap.put(inputId, INPUT_STATE_CONNECTED);
                      for (TvInputCallbackRecord record : mCallbackRecords) {
                          record.postInputAdded(inputId);
                      }
                  }
              }
  
              @Override
              public void onInputRemoved(String inputId) {
                  synchronized (mLock) {
                      mStateMap.remove(inputId);
                      for (TvInputCallbackRecord record : mCallbackRecords) {
                          record.postInputRemoved(inputId);
                      }
                  }
              }
  
              @Override
              public void onInputUpdated(String inputId) {
                  synchronized (mLock) {
                      for (TvInputCallbackRecord record : mCallbackRecords) {
                          record.postInputUpdated(inputId);
                      }
                  }
              }
  
              @Override
              public void onInputStateChanged(String inputId, int state) {
                  synchronized (mLock) {
                      mStateMap.put(inputId, state);
                      for (TvInputCallbackRecord record : mCallbackRecords) {
                          record.postInputStateChanged(inputId, state);
                      }
                  }
              }
  
              @Override
              public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
                  synchronized (mLock) {
                      for (TvInputCallbackRecord record : mCallbackRecords) {
                          record.postTvInputInfoUpdated(inputInfo);
                      }
                  }
              }
          };
          try {
              if (mService != null) {
                  mService.registerCallback(managerCallback, mUserId);
                  List<TvInputInfo> infos = mService.getTvInputList(mUserId);
                  synchronized (mLock) {
                      for (TvInputInfo info : infos) {
                          String inputId = info.getId();
                          mStateMap.put(inputId, mService.getTvInputState(inputId, mUserId));
                      }
                  }
              }
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the complete list of TV inputs on the system.
       *
       * @return List of {@link TvInputInfo} for each TV input that describes its meta information.
       */
      public List<TvInputInfo> getTvInputList() {
          try {
              return mService.getTvInputList(mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the {@link TvInputInfo} for a given TV input.
       *
       * @param inputId The ID of the TV input.
       * @return the {@link TvInputInfo} for a given TV input. {@code null} if not found.
       */
      @Nullable
      public TvInputInfo getTvInputInfo(@NonNull String inputId) {
          Preconditions.checkNotNull(inputId);
          try {
              return mService.getTvInputInfo(inputId, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Updates the <code>TvInputInfo</code> for an existing TV input. A TV input service
       * implementation may call this method to pass the application and system an up-to-date
       * <code>TvInputInfo</code> object that describes itself.
       *
       * <p>The system automatically creates a <code>TvInputInfo</code> object for each TV input,
       * based on the information collected from the <code>AndroidManifest.xml</code>, thus it is not
       * necessary to call this method unless such information has changed dynamically.
       * Use {@link TvInputInfo.Builder} to build a new <code>TvInputInfo</code> object.
       *
       * <p>Attempting to change information about a TV input that the calling package does not own
       * does nothing.
       *
       * @param inputInfo The <code>TvInputInfo</code> object that contains new information.
       * @throws IllegalArgumentException if the argument is {@code null}.
       * @see TvInputCallback#onTvInputInfoUpdated(TvInputInfo)
       */
      public void updateTvInputInfo(@NonNull TvInputInfo inputInfo) {
          Preconditions.checkNotNull(inputInfo);
          try {
              mService.updateTvInputInfo(inputInfo, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the state of a given TV input.
       *
       * <p>The state is one of the following:
       * <ul>
       * <li>{@link #INPUT_STATE_CONNECTED}
       * <li>{@link #INPUT_STATE_CONNECTED_STANDBY}
       * <li>{@link #INPUT_STATE_DISCONNECTED}
       * </ul>
       *
       * @param inputId The ID of the TV input.
       * @throws IllegalArgumentException if the argument is {@code null}.
       */
      @InputState
      public int getInputState(@NonNull String inputId) {
          Preconditions.checkNotNull(inputId);
          synchronized (mLock) {
              Integer state = mStateMap.get(inputId);
              if (state == null) {
                  Log.w(TAG, "Unrecognized input ID: " + inputId);
                  return INPUT_STATE_DISCONNECTED;
              }
              return state;
          }
      }
  
      /**
       * Registers a {@link TvInputCallback}.
       *
       * @param callback A callback used to monitor status of the TV inputs.
       * @param handler A {@link Handler} that the status change will be delivered to.
       */
      public void registerCallback(@NonNull TvInputCallback callback, @NonNull Handler handler) {
          Preconditions.checkNotNull(callback);
          Preconditions.checkNotNull(handler);
          synchronized (mLock) {
              mCallbackRecords.add(new TvInputCallbackRecord(callback, handler));
          }
      }
  
      /**
       * Unregisters the existing {@link TvInputCallback}.
       *
       * @param callback The existing callback to remove.
       */
      public void unregisterCallback(@NonNull final TvInputCallback callback) {
          Preconditions.checkNotNull(callback);
          synchronized (mLock) {
              for (Iterator<TvInputCallbackRecord> it = mCallbackRecords.iterator();
                      it.hasNext(); ) {
                  TvInputCallbackRecord record = it.next();
                  if (record.getCallback() == callback) {
                      it.remove();
                      break;
                  }
              }
          }
      }
  
      /**
       * Returns the user's parental controls enabled state.
       *
       * @return {@code true} if the user enabled the parental controls, {@code false} otherwise.
       */
      public boolean isParentalControlsEnabled() {
          try {
              return mService.isParentalControlsEnabled(mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Sets the user's parental controls enabled state.
       *
       * @param enabled The user's parental controls enabled state. {@code true} if the user enabled
       *            the parental controls, {@code false} otherwise.
       * @see #isParentalControlsEnabled
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
      public void setParentalControlsEnabled(boolean enabled) {
          try {
              mService.setParentalControlsEnabled(enabled, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Checks whether a given TV content rating is blocked by the user.
       *
       * @param rating The TV content rating to check. Can be {@link TvContentRating#UNRATED}.
       * @return {@code true} if the given TV content rating is blocked, {@code false} otherwise.
       */
      public boolean isRatingBlocked(@NonNull TvContentRating rating) {
          Preconditions.checkNotNull(rating);
          try {
              return mService.isRatingBlocked(rating.flattenToString(), mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the list of blocked content ratings.
       *
       * @return the list of content ratings blocked by the user.
       */
      public List<TvContentRating> getBlockedRatings() {
          try {
              List<TvContentRating> ratings = new ArrayList<>();
              for (String rating : mService.getBlockedRatings(mUserId)) {
                  ratings.add(TvContentRating.unflattenFromString(rating));
              }
              return ratings;
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Adds a user blocked content rating.
       *
       * @param rating The content rating to block.
       * @see #isRatingBlocked
       * @see #removeBlockedRating
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
      public void addBlockedRating(@NonNull TvContentRating rating) {
          Preconditions.checkNotNull(rating);
          try {
              mService.addBlockedRating(rating.flattenToString(), mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Removes a user blocked content rating.
       *
       * @param rating The content rating to unblock.
       * @see #isRatingBlocked
       * @see #addBlockedRating
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
      public void removeBlockedRating(@NonNull TvContentRating rating) {
          Preconditions.checkNotNull(rating);
          try {
              mService.removeBlockedRating(rating.flattenToString(), mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the list of all TV content rating systems defined.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.READ_CONTENT_RATING_SYSTEMS)
      public List<TvContentRatingSystemInfo> getTvContentRatingSystemList() {
          try {
              return mService.getTvContentRatingSystemList(mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Notifies the TV input of the given preview program that the program's browsable state is
       * disabled.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.NOTIFY_TV_INPUTS)
      public void notifyPreviewProgramBrowsableDisabled(String packageName, long programId) {
          Intent intent = new Intent();
          intent.setAction(TvContract.ACTION_PREVIEW_PROGRAM_BROWSABLE_DISABLED);
          intent.putExtra(TvContract.EXTRA_PREVIEW_PROGRAM_ID, programId);
          intent.setPackage(packageName);
          try {
              mService.sendTvInputNotifyIntent(intent, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Notifies the TV input of the given watch next program that the program's browsable state is
       * disabled.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.NOTIFY_TV_INPUTS)
      public void notifyWatchNextProgramBrowsableDisabled(String packageName, long programId) {
          Intent intent = new Intent();
          intent.setAction(TvContract.ACTION_WATCH_NEXT_PROGRAM_BROWSABLE_DISABLED);
          intent.putExtra(TvContract.EXTRA_WATCH_NEXT_PROGRAM_ID, programId);
          intent.setPackage(packageName);
          try {
              mService.sendTvInputNotifyIntent(intent, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Notifies the TV input of the given preview program that the program is added to watch next.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.NOTIFY_TV_INPUTS)
      public void notifyPreviewProgramAddedToWatchNext(String packageName, long previewProgramId,
              long watchNextProgramId) {
          Intent intent = new Intent();
          intent.setAction(TvContract.ACTION_PREVIEW_PROGRAM_ADDED_TO_WATCH_NEXT);
          intent.putExtra(TvContract.EXTRA_PREVIEW_PROGRAM_ID, previewProgramId);
          intent.putExtra(TvContract.EXTRA_WATCH_NEXT_PROGRAM_ID, watchNextProgramId);
          intent.setPackage(packageName);
          try {
              mService.sendTvInputNotifyIntent(intent, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Creates a {@link Session} for a given TV input.
       *
       * <p>The number of sessions that can be created at the same time is limited by the capability
       * of the given TV input.
       *
       * @param inputId The ID of the TV input.
       * @param callback A callback used to receive the created session.
       * @param handler A {@link Handler} that the session creation will be delivered to.
       * @hide
       */
      public void createSession(@NonNull String inputId, @NonNull final SessionCallback callback,
              @NonNull Handler handler) {
          createSessionInternal(inputId, false, callback, handler);
      }
  
      /**
       * Creates a recording {@link Session} for a given TV input.
       *
       * <p>The number of sessions that can be created at the same time is limited by the capability
       * of the given TV input.
       *
       * @param inputId The ID of the TV input.
       * @param callback A callback used to receive the created session.
       * @param handler A {@link Handler} that the session creation will be delivered to.
       * @hide
       */
      public void createRecordingSession(@NonNull String inputId,
              @NonNull final SessionCallback callback, @NonNull Handler handler) {
          createSessionInternal(inputId, true, callback, handler);
      }
  
      private void createSessionInternal(String inputId, boolean isRecordingSession,
              SessionCallback callback, Handler handler) {
          Preconditions.checkNotNull(inputId);
          Preconditions.checkNotNull(callback);
          Preconditions.checkNotNull(handler);
          SessionCallbackRecord record = new SessionCallbackRecord(callback, handler);
          synchronized (mSessionCallbackRecordMap) {
              int seq = mNextSeq++;
              mSessionCallbackRecordMap.put(seq, record);
              try {
                  mService.createSession(mClient, inputId, isRecordingSession, seq, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
      }
  
      /**
       * Returns the TvStreamConfig list of the given TV input.
       *
       * If you are using {@link Hardware} object from {@link
       * #acquireTvInputHardware}, you should get the list of available streams
       * from {@link HardwareCallback#onStreamConfigChanged} method, not from
       * here. This method is designed to be used with {@link #captureFrame} in
       * capture scenarios specifically and not suitable for any other use.
       *
       * @param inputId The ID of the TV input.
       * @return List of {@link TvStreamConfig} which is available for capturing
       *   of the given TV input.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.CAPTURE_TV_INPUT)
      public List<TvStreamConfig> getAvailableTvStreamConfigList(String inputId) {
          try {
              return mService.getAvailableTvStreamConfigList(inputId, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Take a snapshot of the given TV input into the provided Surface.
       *
       * @param inputId The ID of the TV input.
       * @param surface the {@link Surface} to which the snapshot is captured.
       * @param config the {@link TvStreamConfig} which is used for capturing.
       * @return true when the {@link Surface} is ready to be captured.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.CAPTURE_TV_INPUT)
      public boolean captureFrame(String inputId, Surface surface, TvStreamConfig config) {
          try {
              return mService.captureFrame(inputId, surface, config, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns true if there is only a single TV input session.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.CAPTURE_TV_INPUT)
      public boolean isSingleSessionActive() {
          try {
              return mService.isSingleSessionActive(mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns a list of TvInputHardwareInfo objects representing available hardware.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
      public List<TvInputHardwareInfo> getHardwareList() {
          try {
              return mService.getHardwareList();
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Acquires {@link Hardware} object for the given device ID.
       *
       * <p>A subsequent call to this method on the same {@code deviceId} will release the currently
       * acquired Hardware.
       *
       * @param deviceId The device ID to acquire Hardware for.
       * @param callback A callback to receive updates on Hardware.
       * @param info The TV input which will use the acquired Hardware.
       * @return Hardware on success, {@code null} otherwise.
       *
       * @hide
       * @removed
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
      public Hardware acquireTvInputHardware(int deviceId, final HardwareCallback callback,
              TvInputInfo info) {
          return acquireTvInputHardware(deviceId, info, callback);
      }
  
      /**
       * Acquires {@link Hardware} object for the given device ID.
       *
       * <p>A subsequent call to this method on the same {@code deviceId} will release the currently
       * acquired Hardware.
       *
       * @param deviceId The device ID to acquire Hardware for.
       * @param callback A callback to receive updates on Hardware.
       * @param info The TV input which will use the acquired Hardware.
       * @return Hardware on success, {@code null} otherwise.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
      public Hardware acquireTvInputHardware(int deviceId, TvInputInfo info,
              final HardwareCallback callback) {
          try {
              return new Hardware(
                      mService.acquireTvInputHardware(deviceId, new ITvInputHardwareCallback.Stub() {
                  @Override
                  public void onReleased() {
                      callback.onReleased();
                  }
  
                  @Override
                  public void onStreamConfigChanged(TvStreamConfig[] configs) {
                      callback.onStreamConfigChanged(configs);
                  }
              }, info, mUserId));
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Releases previously acquired hardware object.
       *
       * @param deviceId The device ID this Hardware was acquired for
       * @param hardware Hardware to release.
       *
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.TV_INPUT_HARDWARE)
      public void releaseTvInputHardware(int deviceId, Hardware hardware) {
          try {
              mService.releaseTvInputHardware(deviceId, hardware.getInterface(), mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns the list of currently available DVB devices on the system.
       *
       * @return the list of {@link DvbDeviceInfo} objects representing available DVB devices.
       * @hide
       */
      public List<DvbDeviceInfo> getDvbDeviceList() {
          try {
              return mService.getDvbDeviceList();
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Returns a {@link ParcelFileDescriptor} of a specified DVB device for a given
       * {@link DvbDeviceInfo}
       *
       * @param info A {@link DvbDeviceInfo} to open a DVB device.
       * @param device A DVB device. The DVB device can be {@link #DVB_DEVICE_DEMUX},
       *            {@link #DVB_DEVICE_DVR} or {@link #DVB_DEVICE_FRONTEND}.
       * @return a {@link ParcelFileDescriptor} of a specified DVB device for a given
       *         {@link DvbDeviceInfo}, or {@code null} if the given {@link DvbDeviceInfo} was invalid
       *         or the specified DVB device was busy with a previous request.
       * @hide
       */
      public ParcelFileDescriptor openDvbDevice(DvbDeviceInfo info, int device) {
          try {
              if (DVB_DEVICE_START > device || DVB_DEVICE_END < device) {
                  throw new IllegalArgumentException("Invalid DVB device: " + device);
              }
              return mService.openDvbDevice(info, device);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * Requests to make a channel browsable.
       *
       * <p>Once called, the system will review the request and make the channel browsable based on
       * its policy. The first request from a package is guaranteed to be approved.
       *
       * @param channelUri The URI for the channel to be browsable.
       * @hide
       */
      public void requestChannelBrowsable(Uri channelUri) {
          try {
              mService.requestChannelBrowsable(channelUri, mUserId);
          } catch (RemoteException e) {
              throw e.rethrowFromSystemServer();
          }
      }
  
      /**
       * The Session provides the per-session functionality of TV inputs.
       * @hide
       */
      public static final class Session {
          static final int DISPATCH_IN_PROGRESS = -1;
          static final int DISPATCH_NOT_HANDLED = 0;
          static final int DISPATCH_HANDLED = 1;
  
          private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500;
  
          private final ITvInputManager mService;
          private final int mUserId;
          private final int mSeq;
  
          // For scheduling input event handling on the main thread. This also serves as a lock to
          // protect pending input events and the input channel.
          private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper());
  
          private final Pool<PendingEvent> mPendingEventPool = new SimplePool<>(20);
          private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20);
          private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap;
  
          private IBinder mToken;
          private TvInputEventSender mSender;
          private InputChannel mChannel;
  
          private final Object mMetadataLock = new Object();
          // @GuardedBy("mMetadataLock")
          private final List<TvTrackInfo> mAudioTracks = new ArrayList<>();
          // @GuardedBy("mMetadataLock")
          private final List<TvTrackInfo> mVideoTracks = new ArrayList<>();
          // @GuardedBy("mMetadataLock")
          private final List<TvTrackInfo> mSubtitleTracks = new ArrayList<>();
          // @GuardedBy("mMetadataLock")
          private String mSelectedAudioTrackId;
          // @GuardedBy("mMetadataLock")
          private String mSelectedVideoTrackId;
          // @GuardedBy("mMetadataLock")
          private String mSelectedSubtitleTrackId;
          // @GuardedBy("mMetadataLock")
          private int mVideoWidth;
          // @GuardedBy("mMetadataLock")
          private int mVideoHeight;
  
          private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
                  int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) {
              mToken = token;
              mChannel = channel;
              mService = service;
              mUserId = userId;
              mSeq = seq;
              mSessionCallbackRecordMap = sessionCallbackRecordMap;
          }
  
          /**
           * Releases this session.
           */
          public void release() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.releaseSession(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
  
              releaseInternal();
          }
  
          /**
           * Sets this as the main session. The main session is a session whose corresponding TV
           * input determines the HDMI-CEC active source device.
           *
           * @see TvView#setMain
           */
          void setMain() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.setMainSession(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Sets the {@link android.view.Surface} for this session.
           *
           * @param surface A {@link android.view.Surface} used to render video.
           */
          public void setSurface(Surface surface) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              // surface can be null.
              try {
                  mService.setSurface(mToken, surface, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Notifies of any structural changes (format or size) of the surface passed in
           * {@link #setSurface}.
           *
           * @param format The new PixelFormat of the surface.
           * @param width The new width of the surface.
           * @param height The new height of the surface.
           */
          public void dispatchSurfaceChanged(int format, int width, int height) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Sets the relative stream volume of this session to handle a change of audio focus.
           *
           * @param volume A volume value between 0.0f to 1.0f.
           * @throws IllegalArgumentException if the volume value is out of range.
           */
          public void setStreamVolume(float volume) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  if (volume < 0.0f || volume > 1.0f) {
                      throw new IllegalArgumentException("volume should be between 0.0f and 1.0f");
                  }
                  mService.setVolume(mToken, volume, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Tunes to a given channel.
           *
           * @param channelUri The URI of a channel.
           */
          public void tune(Uri channelUri) {
              tune(channelUri, null);
          }
  
          /**
           * Tunes to a given channel.
           *
           * @param channelUri The URI of a channel.
           * @param params A set of extra parameters which might be handled with this tune event.
           */
          public void tune(@NonNull Uri channelUri, Bundle params) {
              Preconditions.checkNotNull(channelUri);
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              synchronized (mMetadataLock) {
                  mAudioTracks.clear();
                  mVideoTracks.clear();
                  mSubtitleTracks.clear();
                  mSelectedAudioTrackId = null;
                  mSelectedVideoTrackId = null;
                  mSelectedSubtitleTrackId = null;
                  mVideoWidth = 0;
                  mVideoHeight = 0;
              }
              try {
                  mService.tune(mToken, channelUri, params, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Enables or disables the caption for this session.
           *
           * @param enabled {@code true} to enable, {@code false} to disable.
           */
          public void setCaptionEnabled(boolean enabled) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.setCaptionEnabled(mToken, enabled, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Selects a track.
           *
           * @param type The type of the track to select. The type can be
           *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
           *            {@link TvTrackInfo#TYPE_SUBTITLE}.
           * @param trackId The ID of the track to select. When {@code null}, the currently selected
           *            track of the given type will be unselected.
           * @see #getTracks
           */
          public void selectTrack(int type, @Nullable String trackId) {
              synchronized (mMetadataLock) {
                  if (type == TvTrackInfo.TYPE_AUDIO) {
                      if (trackId != null && !containsTrack(mAudioTracks, trackId)) {
                          Log.w(TAG, "Invalid audio trackId: " + trackId);
                          return;
                      }
                  } else if (type == TvTrackInfo.TYPE_VIDEO) {
                      if (trackId != null && !containsTrack(mVideoTracks, trackId)) {
                          Log.w(TAG, "Invalid video trackId: " + trackId);
                          return;
                      }
                  } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
                      if (trackId != null && !containsTrack(mSubtitleTracks, trackId)) {
                          Log.w(TAG, "Invalid subtitle trackId: " + trackId);
                          return;
                      }
                  } else {
                      throw new IllegalArgumentException("invalid type: " + type);
                  }
              }
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.selectTrack(mToken, type, trackId, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          private boolean containsTrack(List<TvTrackInfo> tracks, String trackId) {
              for (TvTrackInfo track : tracks) {
                  if (track.getId().equals(trackId)) {
                      return true;
                  }
              }
              return false;
          }
  
          /**
           * Returns the list of tracks for a given type. Returns {@code null} if the information is
           * not available.
           *
           * @param type The type of the tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
           *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
           * @return the list of tracks for the given type.
           */
          @Nullable
          public List<TvTrackInfo> getTracks(int type) {
              synchronized (mMetadataLock) {
                  if (type == TvTrackInfo.TYPE_AUDIO) {
                      if (mAudioTracks == null) {
                          return null;
                      }
                      return new ArrayList<>(mAudioTracks);
                  } else if (type == TvTrackInfo.TYPE_VIDEO) {
                      if (mVideoTracks == null) {
                          return null;
                      }
                      return new ArrayList<>(mVideoTracks);
                  } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
                      if (mSubtitleTracks == null) {
                          return null;
                      }
                      return new ArrayList<>(mSubtitleTracks);
                  }
              }
              throw new IllegalArgumentException("invalid type: " + type);
          }
  
          /**
           * Returns the selected track for a given type. Returns {@code null} if the information is
           * not available or any of the tracks for the given type is not selected.
           *
           * @return The ID of the selected track.
           * @see #selectTrack
           */
          @Nullable
          public String getSelectedTrack(int type) {
              synchronized (mMetadataLock) {
                  if (type == TvTrackInfo.TYPE_AUDIO) {
                      return mSelectedAudioTrackId;
                  } else if (type == TvTrackInfo.TYPE_VIDEO) {
                      return mSelectedVideoTrackId;
                  } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
                      return mSelectedSubtitleTrackId;
                  }
              }
              throw new IllegalArgumentException("invalid type: " + type);
          }
  
          /**
           * Responds to onTracksChanged() and updates the internal track information. Returns true if
           * there is an update.
           */
          boolean updateTracks(List<TvTrackInfo> tracks) {
              synchronized (mMetadataLock) {
                  mAudioTracks.clear();
                  mVideoTracks.clear();
                  mSubtitleTracks.clear();
                  for (TvTrackInfo track : tracks) {
                      if (track.getType() == TvTrackInfo.TYPE_AUDIO) {
                          mAudioTracks.add(track);
                      } else if (track.getType() == TvTrackInfo.TYPE_VIDEO) {
                          mVideoTracks.add(track);
                      } else if (track.getType() == TvTrackInfo.TYPE_SUBTITLE) {
                          mSubtitleTracks.add(track);
                      }
                  }
                  return !mAudioTracks.isEmpty() || !mVideoTracks.isEmpty()
                          || !mSubtitleTracks.isEmpty();
              }
          }
  
          /**
           * Responds to onTrackSelected() and updates the internal track selection information.
           * Returns true if there is an update.
           */
          boolean updateTrackSelection(int type, String trackId) {
              synchronized (mMetadataLock) {
                  if (type == TvTrackInfo.TYPE_AUDIO
                          && !TextUtils.equals(trackId, mSelectedAudioTrackId)) {
                      mSelectedAudioTrackId = trackId;
                      return true;
                  } else if (type == TvTrackInfo.TYPE_VIDEO
                          && !TextUtils.equals(trackId, mSelectedVideoTrackId)) {
                      mSelectedVideoTrackId = trackId;
                      return true;
                  } else if (type == TvTrackInfo.TYPE_SUBTITLE
                          && !TextUtils.equals(trackId, mSelectedSubtitleTrackId)) {
                      mSelectedSubtitleTrackId = trackId;
                      return true;
                  }
              }
              return false;
          }
  
          /**
           * Returns the new/updated video track that contains new video size information. Returns
           * null if there is no video track to notify. Subsequent calls of this method results in a
           * non-null video track returned only by the first call and null returned by following
           * calls. The caller should immediately notify of the video size change upon receiving the
           * track.
           */
          TvTrackInfo getVideoTrackToNotify() {
              synchronized (mMetadataLock) {
                  if (!mVideoTracks.isEmpty() && mSelectedVideoTrackId != null) {
                      for (TvTrackInfo track : mVideoTracks) {
                          if (track.getId().equals(mSelectedVideoTrackId)) {
                              int videoWidth = track.getVideoWidth();
                              int videoHeight = track.getVideoHeight();
                              if (mVideoWidth != videoWidth || mVideoHeight != videoHeight) {
                                  mVideoWidth = videoWidth;
                                  mVideoHeight = videoHeight;
                                  return track;
                              }
                          }
                      }
                  }
              }
              return null;
          }
  
          /**
           * Plays a given recorded TV program.
           */
          void timeShiftPlay(Uri recordedProgramUri) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftPlay(mToken, recordedProgramUri, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Pauses the playback. Call {@link #timeShiftResume()} to restart the playback.
           */
          void timeShiftPause() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftPause(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Resumes the playback. No-op if it is already playing the channel.
           */
          void timeShiftResume() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftResume(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Seeks to a specified time position.
           *
           * <p>Normally, the position is given within range between the start and the current time,
           * inclusively.
           *
           * @param timeMs The time position to seek to, in milliseconds since the epoch.
           * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged
           */
          void timeShiftSeekTo(long timeMs) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftSeekTo(mToken, timeMs, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Sets playback rate using {@link android.media.PlaybackParams}.
           *
           * @param params The playback params.
           */
          void timeShiftSetPlaybackParams(PlaybackParams params) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftSetPlaybackParams(mToken, params, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Enable/disable position tracking.
           *
           * @param enable {@code true} to enable tracking, {@code false} otherwise.
           */
          void timeShiftEnablePositionTracking(boolean enable) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.timeShiftEnablePositionTracking(mToken, enable, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Starts TV program recording in the current recording session.
           *
           * @param programUri The URI for the TV program to record as a hint, built by
           *            {@link TvContract#buildProgramUri(long)}. Can be {@code null}.
           */
          void startRecording(@Nullable Uri programUri) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.startRecording(mToken, programUri, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Stops TV program recording in the current recording session.
           */
          void stopRecording() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.stopRecording(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
           * TvInputService.Session.appPrivateCommand()} on the current TvView.
           *
           * @param action Name of the command to be performed. This <em>must</em> be a scoped name,
           *            i.e. prefixed with a package name you own, so that different developers will
           *            not create conflicting commands.
           * @param data Any data to include with the command.
           */
          public void sendAppPrivateCommand(String action, Bundle data) {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.sendAppPrivateCommand(mToken, action, data, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Creates an overlay view. Once the overlay view is created, {@link #relayoutOverlayView}
           * should be called whenever the layout of its containing view is changed.
           * {@link #removeOverlayView()} should be called to remove the overlay view.
           * Since a session can have only one overlay view, this method should be called only once
           * or it can be called again after calling {@link #removeOverlayView()}.
           *
           * @param view A view playing TV.
           * @param frame A position of the overlay view.
           * @throws IllegalStateException if {@code view} is not attached to a window.
           */
          void createOverlayView(@NonNull View view, @NonNull Rect frame) {
              Preconditions.checkNotNull(view);
              Preconditions.checkNotNull(frame);
              if (view.getWindowToken() == null) {
                  throw new IllegalStateException("view must be attached to a window");
              }
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.createOverlayView(mToken, view.getWindowToken(), frame, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Relayouts the current overlay view.
           *
           * @param frame A new position of the overlay view.
           */
          void relayoutOverlayView(@NonNull Rect frame) {
              Preconditions.checkNotNull(frame);
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.relayoutOverlayView(mToken, frame, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Removes the current overlay view.
           */
          void removeOverlayView() {
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.removeOverlayView(mToken, mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Requests to unblock content blocked by parental controls.
           */
          void unblockContent(@NonNull TvContentRating unblockedRating) {
              Preconditions.checkNotNull(unblockedRating);
              if (mToken == null) {
                  Log.w(TAG, "The session has been already released");
                  return;
              }
              try {
                  mService.unblockContent(mToken, unblockedRating.flattenToString(), mUserId);
              } catch (RemoteException e) {
                  throw e.rethrowFromSystemServer();
              }
          }
  
          /**
           * Dispatches an input event to this session.
           *
           * @param event An {@link InputEvent} to dispatch. Cannot be {@code null}.
           * @param token A token used to identify the input event later in the callback.
           * @param callback A callback used to receive the dispatch result. Cannot be {@code null}.
           * @param handler A {@link Handler} that the dispatch result will be delivered to. Cannot be
           *            {@code null}.
           * @return Returns {@link #DISPATCH_HANDLED} if the event was handled. Returns
           *         {@link #DISPATCH_NOT_HANDLED} if the event was not handled. Returns
           *         {@link #DISPATCH_IN_PROGRESS} if the event is in progress and the callback will
           *         be invoked later.
           * @hide
           */
          public int dispatchInputEvent(@NonNull InputEvent event, Object token,
                  @NonNull FinishedInputEventCallback callback, @NonNull Handler handler) {
              Preconditions.checkNotNull(event);
              Preconditions.checkNotNull(callback);
              Preconditions.checkNotNull(handler);
              synchronized (mHandler) {
                  if (mChannel == null) {
                      return DISPATCH_NOT_HANDLED;
                  }
                  PendingEvent p = obtainPendingEventLocked(event, token, callback, handler);
                  if (Looper.myLooper() == Looper.getMainLooper()) {
                      // Already running on the main thread so we can send the event immediately.
                      return sendInputEventOnMainLooperLocked(p);
                  }
  
                  // Post the event to the main thread.
                  Message msg = mHandler.obtainMessage(InputEventHandler.MSG_SEND_INPUT_EVENT, p);
                  msg.setAsynchronous(true);
                  mHandler.sendMessage(msg);
                  return DISPATCH_IN_PROGRESS;
              }
          }
  
          /**
           * Callback that is invoked when an input event that was dispatched to this session has been
           * finished.
           *
           * @hide
           */
          public interface FinishedInputEventCallback {
              /**
               * Called when the dispatched input event is finished.
               *
               * @param token A token passed to {@link #dispatchInputEvent}.
               * @param handled {@code true} if the dispatched input event was handled properly.
               *            {@code false} otherwise.
               */
              void onFinishedInputEvent(Object token, boolean handled);
          }
  
          // Must be called on the main looper
          private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) {
              synchronized (mHandler) {
                  int result = sendInputEventOnMainLooperLocked(p);
                  if (result == DISPATCH_IN_PROGRESS) {
                      return;
                  }
              }
  
              invokeFinishedInputEventCallback(p, false);
          }
  
          private int sendInputEventOnMainLooperLocked(PendingEvent p) {
              if (mChannel != null) {
                  if (mSender == null) {
                      mSender = new TvInputEventSender(mChannel, mHandler.getLooper());
                  }
  
                  final InputEvent event = p.mEvent;
                  final int seq = event.getSequenceNumber();
                  if (mSender.sendInputEvent(seq, event)) {
                      mPendingEvents.put(seq, p);
                      Message msg = mHandler.obtainMessage(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
                      msg.setAsynchronous(true);
                      mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT);
                      return DISPATCH_IN_PROGRESS;
                  }
  
                  Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:"
                          + event);
              }
              return DISPATCH_NOT_HANDLED;
          }
  
          void finishedInputEvent(int seq, boolean handled, boolean timeout) {
              final PendingEvent p;
              synchronized (mHandler) {
                  int index = mPendingEvents.indexOfKey(seq);
                  if (index < 0) {
                      return; // spurious, event already finished or timed out
                  }
  
                  p = mPendingEvents.valueAt(index);
                  mPendingEvents.removeAt(index);
  
                  if (timeout) {
                      Log.w(TAG, "Timeout waiting for session to handle input event after "
                              + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken);
                  } else {
                      mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p);
                  }
              }
  
              invokeFinishedInputEventCallback(p, handled);
          }
  
          // Assumes the event has already been removed from the queue.
          void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
              p.mHandled = handled;
              if (p.mEventHandler.getLooper().isCurrentThread()) {
                  // Already running on the callback handler thread so we can send the callback
                  // immediately.
                  p.run();
              } else {
                  // Post the event to the callback handler thread.
                  // In this case, the callback will be responsible for recycling the event.
                  Message msg = Message.obtain(p.mEventHandler, p);
                  msg.setAsynchronous(true);
                  msg.sendToTarget();
              }
          }
  
          private void flushPendingEventsLocked() {
              mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT);
  
              final int count = mPendingEvents.size();
              for (int i = 0; i < count; i++) {
                  int seq = mPendingEvents.keyAt(i);
                  Message msg = mHandler.obtainMessage(InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0);
                  msg.setAsynchronous(true);
                  msg.sendToTarget();
              }
          }
  
          private PendingEvent obtainPendingEventLocked(InputEvent event, Object token,
                  FinishedInputEventCallback callback, Handler handler) {
              PendingEvent p = mPendingEventPool.acquire();
              if (p == null) {
                  p = new PendingEvent();
              }
              p.mEvent = event;
              p.mEventToken = token;
              p.mCallback = callback;
              p.mEventHandler = handler;
              return p;
          }
  
          private void recyclePendingEventLocked(PendingEvent p) {
              p.recycle();
              mPendingEventPool.release(p);
          }
  
          IBinder getToken() {
              return mToken;
          }
  
          private void releaseInternal() {
              mToken = null;
              synchronized (mHandler) {
                  if (mChannel != null) {
                      if (mSender != null) {
                          flushPendingEventsLocked();
                          mSender.dispose();
                          mSender = null;
                      }
                      mChannel.dispose();
                      mChannel = null;
                  }
              }
              synchronized (mSessionCallbackRecordMap) {
                  mSessionCallbackRecordMap.delete(mSeq);
              }
          }
  
          private final class InputEventHandler extends Handler {
              public static final int MSG_SEND_INPUT_EVENT = 1;
              public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
              public static final int MSG_FLUSH_INPUT_EVENT = 3;
  
              InputEventHandler(Looper looper) {
                  super(looper, null, true);
              }
  
              @Override
              public void handleMessage(Message msg) {
                  switch (msg.what) {
                      case MSG_SEND_INPUT_EVENT: {
                          sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj);
                          return;
                      }
                      case MSG_TIMEOUT_INPUT_EVENT: {
                          finishedInputEvent(msg.arg1, false, true);
                          return;
                      }
                      case MSG_FLUSH_INPUT_EVENT: {
                          finishedInputEvent(msg.arg1, false, false);
                          return;
                      }
                  }
              }
          }
  
          private final class TvInputEventSender extends InputEventSender {
              public TvInputEventSender(InputChannel inputChannel, Looper looper) {
                  super(inputChannel, looper);
              }
  
              @Override
              public void onInputEventFinished(int seq, boolean handled) {
                  finishedInputEvent(seq, handled, false);
              }
          }
  
          private final class PendingEvent implements Runnable {
              public InputEvent mEvent;
              public Object mEventToken;
              public FinishedInputEventCallback mCallback;
              public Handler mEventHandler;
              public boolean mHandled;
  
              public void recycle() {
                  mEvent = null;
                  mEventToken = null;
                  mCallback = null;
                  mEventHandler = null;
                  mHandled = false;
              }
  
              @Override
              public void run() {
                  mCallback.onFinishedInputEvent(mEventToken, mHandled);
  
                  synchronized (mEventHandler) {
                      recyclePendingEventLocked(this);
                  }
              }
          }
      }
  
      /**
       * The Hardware provides the per-hardware functionality of TV hardware.
       *
       * <p>TV hardware is physical hardware attached to the Android device; for example, HDMI ports,
       * Component/Composite ports, etc. Specifically, logical devices such as HDMI CEC logical
       * devices don't fall into this category.
       *
       * @hide
       */
      @SystemApi
      public final static class Hardware {
          private final ITvInputHardware mInterface;
  
          private Hardware(ITvInputHardware hardwareInterface) {
              mInterface = hardwareInterface;
          }
  
          private ITvInputHardware getInterface() {
              return mInterface;
          }
  
          public boolean setSurface(Surface surface, TvStreamConfig config) {
              try {
                  return mInterface.setSurface(surface, config);
              } catch (RemoteException e) {
                  throw new RuntimeException(e);
              }
          }
  
          public void setStreamVolume(float volume) {
              try {
                  mInterface.setStreamVolume(volume);
              } catch (RemoteException e) {
                  throw new RuntimeException(e);
              }
          }
  
          /** @removed */
          @SystemApi
          public boolean dispatchKeyEventToHdmi(KeyEvent event) {
              return false;
          }
  
          public void overrideAudioSink(int audioType, String audioAddress, int samplingRate,
                  int channelMask, int format) {
              try {
                  mInterface.overrideAudioSink(audioType, audioAddress, samplingRate, channelMask,
                          format);
              } catch (RemoteException e) {
                  throw new RuntimeException(e);
              }
          }
      }
  }
  