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.FloatRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.RequiresPermission;
  import android.annotation.SystemApi;
  import android.content.Context;
  import android.content.pm.PackageManager;
  import android.graphics.Canvas;
  import android.graphics.PorterDuff;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Region;
  import android.media.PlaybackParams;
  import android.media.tv.TvInputManager.Session;
  import android.media.tv.TvInputManager.Session.FinishedInputEventCallback;
  import android.media.tv.TvInputManager.SessionCallback;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.Handler;
  import android.text.TextUtils;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.Pair;
  import android.view.InputEvent;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.Surface;
  import android.view.SurfaceHolder;
  import android.view.SurfaceView;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.ViewRootImpl;
  
  import java.lang.ref.WeakReference;
  import java.util.ArrayDeque;
  import java.util.List;
  import java.util.Queue;
  
  /**
   * Displays TV contents. The TvView class provides a high level interface for applications to show
   * TV programs from various TV sources that implement {@link TvInputService}. (Note that the list of
   * TV inputs available on the system can be obtained by calling
   * {@link TvInputManager#getTvInputList() TvInputManager.getTvInputList()}.)
   *
   * <p>Once the application supplies the URI for a specific TV channel to {@link #tune}
   * method, it takes care of underlying service binding (and unbinding if the current TvView is
   * already bound to a service) and automatically allocates/deallocates resources needed. In addition
   * to a few essential methods to control how the contents are presented, it also provides a way to
   * dispatch input events to the connected TvInputService in order to enable custom key actions for
   * the TV input.
   */
  public class TvView extends ViewGroup {
      private static final String TAG = "TvView";
      private static final boolean DEBUG = false;
  
      private static final int ZORDER_MEDIA = 0;
      private static final int ZORDER_MEDIA_OVERLAY = 1;
      private static final int ZORDER_ON_TOP = 2;
  
      private static final WeakReference<TvView> NULL_TV_VIEW = new WeakReference<>(null);
  
      private static final Object sMainTvViewLock = new Object();
      private static WeakReference<TvView> sMainTvView = NULL_TV_VIEW;
  
      private final Handler mHandler = new Handler();
      private Session mSession;
      private SurfaceView mSurfaceView;
      private Surface mSurface;
      private boolean mOverlayViewCreated;
      private Rect mOverlayViewFrame;
      private final TvInputManager mTvInputManager;
      private MySessionCallback mSessionCallback;
      private TvInputCallback mCallback;
      private OnUnhandledInputEventListener mOnUnhandledInputEventListener;
      private Float mStreamVolume;
      private Boolean mCaptionEnabled;
      private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>();
  
      private boolean mSurfaceChanged;
      private int mSurfaceFormat;
      private int mSurfaceWidth;
      private int mSurfaceHeight;
      private final AttributeSet mAttrs;
      private final int mDefStyleAttr;
      private int mWindowZOrder;
      private boolean mUseRequestedSurfaceLayout;
      private int mSurfaceViewLeft;
      private int mSurfaceViewRight;
      private int mSurfaceViewTop;
      private int mSurfaceViewBottom;
      private TimeShiftPositionCallback mTimeShiftPositionCallback;
  
      private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
          @Override
          public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
              if (DEBUG) {
                  Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + ", width="
                      + width + ", height=" + height + ")");
              }
              mSurfaceFormat = format;
              mSurfaceWidth = width;
              mSurfaceHeight = height;
              mSurfaceChanged = true;
              dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
          }
  
          @Override
          public void surfaceCreated(SurfaceHolder holder) {
              mSurface = holder.getSurface();
              setSessionSurface(mSurface);
          }
  
          @Override
          public void surfaceDestroyed(SurfaceHolder holder) {
              mSurface = null;
              mSurfaceChanged = false;
              setSessionSurface(null);
          }
      };
  
      private final FinishedInputEventCallback mFinishedInputEventCallback =
              new FinishedInputEventCallback() {
          @Override
          public void onFinishedInputEvent(Object token, boolean handled) {
              if (DEBUG) {
                  Log.d(TAG, "onFinishedInputEvent(token=" + token + ", handled=" + handled + ")");
              }
              if (handled) {
                  return;
              }
              // TODO: Re-order unhandled events.
              InputEvent event = (InputEvent) token;
              if (dispatchUnhandledInputEvent(event)) {
                  return;
              }
              ViewRootImpl viewRootImpl = getViewRootImpl();
              if (viewRootImpl != null) {
                  viewRootImpl.dispatchUnhandledInputEvent(event);
              }
          }
      };
  
      public TvView(Context context) {
          this(context, null, 0);
      }
  
      public TvView(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public TvView(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          mAttrs = attrs;
          mDefStyleAttr = defStyleAttr;
          resetSurfaceView();
          mTvInputManager = (TvInputManager) getContext().getSystemService(Context.TV_INPUT_SERVICE);
      }
  
      /**
       * Sets the callback to be invoked when an event is dispatched to this TvView.
       *
       * @param callback The callback to receive events. A value of {@code null} removes the existing
       *            callback.
       */
      public void setCallback(@Nullable TvInputCallback callback) {
          mCallback = callback;
      }
  
      /**
       * Sets this as the main {@link TvView}.
       *
       * <p>The main {@link TvView} is a {@link TvView} whose corresponding TV input determines the
       * HDMI-CEC active source device. For an HDMI port input, one of source devices that is
       * connected to that HDMI port becomes the active source. For an HDMI-CEC logical device input,
       * the corresponding HDMI-CEC logical device becomes the active source. For any non-HDMI input
       * (including the tuner, composite, S-Video, etc.), the internal device (= TV itself) becomes
       * the active source.
       *
       * <p>First tuned {@link TvView} becomes main automatically, and keeps to be main until either
       * {@link #reset} is called for the main {@link TvView} or {@code setMain()} is called for other
       * {@link TvView}.
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE)
      public void setMain() {
          synchronized (sMainTvViewLock) {
              sMainTvView = new WeakReference<>(this);
              if (hasWindowFocus() && mSession != null) {
                  mSession.setMain();
              }
          }
      }
  
      /**
       * Controls whether the TvView's surface is placed on top of another regular surface view in the
       * window (but still behind the window itself).
       * This is typically used to place overlays on top of an underlying TvView.
       *
       * <p>Note that this must be set before the TvView's containing window is attached to the
       * window manager.
       *
       * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
       *
       * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false}
       *            otherwise.
       */
      public void setZOrderMediaOverlay(boolean isMediaOverlay) {
          if (isMediaOverlay) {
              mWindowZOrder = ZORDER_MEDIA_OVERLAY;
              removeSessionOverlayView();
          } else {
              mWindowZOrder = ZORDER_MEDIA;
              createSessionOverlayView();
          }
          if (mSurfaceView != null) {
              // ZOrderOnTop(false) removes WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
              // from WindowLayoutParam as well as changes window type.
              mSurfaceView.setZOrderOnTop(false);
              mSurfaceView.setZOrderMediaOverlay(isMediaOverlay);
          }
      }
  
      /**
       * Controls whether the TvView's surface is placed on top of its window. Normally it is placed
       * behind the window, to allow it to (for the most part) appear to composite with the views in
       * the hierarchy.  By setting this, you cause it to be placed above the window. This means that
       * none of the contents of the window this TvView is in will be visible on top of its surface.
       *
       * <p>Note that this must be set before the TvView's containing window is attached to the window
       * manager.
       *
       * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
       *
       * @param onTop {@code true} to be on top of its window, {@code false} otherwise.
       */
      public void setZOrderOnTop(boolean onTop) {
          if (onTop) {
              mWindowZOrder = ZORDER_ON_TOP;
              removeSessionOverlayView();
          } else {
              mWindowZOrder = ZORDER_MEDIA;
              createSessionOverlayView();
          }
          if (mSurfaceView != null) {
              mSurfaceView.setZOrderMediaOverlay(false);
              mSurfaceView.setZOrderOnTop(onTop);
          }
       }
  
      /**
       * Sets the relative stream volume of this TvView.
       *
       * <p>This method is primarily used to handle audio focus changes or mute a specific TvView when
       * multiple views are displayed. If the method has not yet been called, the TvView assumes the
       * default value of {@code 1.0f}.
       *
       * @param volume A volume value between {@code 0.0f} to {@code 1.0f}.
       */
      public void setStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume) {
          if (DEBUG) Log.d(TAG, "setStreamVolume(" + volume + ")");
          mStreamVolume = volume;
          if (mSession == null) {
              // Volume will be set once the connection has been made.
              return;
          }
          mSession.setStreamVolume(volume);
      }
  
      /**
       * Tunes to a given channel.
       *
       * @param inputId The ID of the TV input for the given channel.
       * @param channelUri The URI of a channel.
       */
      public void tune(@NonNull String inputId, Uri channelUri) {
          tune(inputId, channelUri, null);
      }
  
      /**
       * Tunes to a given channel. This can be used to provide domain-specific features that are only
       * known between certain clients and their TV inputs.
       *
       * @param inputId The ID of TV input for the given channel.
       * @param channelUri The URI of a channel.
       * @param params Domain-specific data for this tune request. Keys <em>must</em> be a scoped
       *            name, i.e. prefixed with a package name you own, so that different developers will
       *            not create conflicting keys.
       */
      public void tune(String inputId, Uri channelUri, Bundle params) {
          if (DEBUG) Log.d(TAG, "tune(" + channelUri + ")");
          if (TextUtils.isEmpty(inputId)) {
              throw new IllegalArgumentException("inputId cannot be null or an empty string");
          }
          synchronized (sMainTvViewLock) {
              if (sMainTvView.get() == null) {
                  sMainTvView = new WeakReference<>(this);
              }
          }
          if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) {
              if (mSession != null) {
                  mSession.tune(channelUri, params);
              } else {
                  // createSession() was called but the actual session for the given inputId has not
                  // yet been created. Just replace the existing tuning params in the callback with
                  // the new ones and tune later in onSessionCreated(). It is not necessary to create
                  // a new callback because this tuning request was made on the same inputId.
                  mSessionCallback.mChannelUri = channelUri;
                  mSessionCallback.mTuneParams = params;
              }
          } else {
              resetInternal();
              // In case createSession() is called multiple times across different inputId's before
              // any session is created (e.g. when quickly tuning to a channel from input A and then
              // to another channel from input B), only the callback for the last createSession()
              // should be invoked. (The previous callbacks are simply ignored.) To do that, we create
              // a new callback each time and keep mSessionCallback pointing to the last one. If
              // MySessionCallback.this is different from mSessionCallback, we know that this callback
              // is obsolete and should ignore it.
              mSessionCallback = new MySessionCallback(inputId, channelUri, params);
              if (mTvInputManager != null) {
                  mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
              }
          }
      }
  
      /**
       * Resets this TvView.
       *
       * <p>This method is primarily used to un-tune the current TvView.
       */
      public void reset() {
          if (DEBUG) Log.d(TAG, "reset()");
          synchronized (sMainTvViewLock) {
              if (this == sMainTvView.get()) {
                  sMainTvView = NULL_TV_VIEW;
              }
          }
          resetInternal();
      }
  
      private void resetInternal() {
          mSessionCallback = null;
          mPendingAppPrivateCommands.clear();
          if (mSession != null) {
              setSessionSurface(null);
              removeSessionOverlayView();
              mUseRequestedSurfaceLayout = false;
              mSession.release();
              mSession = null;
              resetSurfaceView();
          }
      }
  
      /**
       * Requests to unblock TV content according to the given rating.
       *
       * <p>This notifies TV input that blocked content is now OK to play.
       *
       * @param unblockedRating A TvContentRating to unblock.
       * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
       * @removed
       */
      public void requestUnblockContent(TvContentRating unblockedRating) {
          unblockContent(unblockedRating);
      }
  
      /**
       * Requests to unblock TV content according to the given rating.
       *
       * <p>This notifies TV input that blocked content is now OK to play.
       *
       * @param unblockedRating A TvContentRating to unblock.
       * @see TvInputService.Session#notifyContentBlocked(TvContentRating)
       * @hide
       */
      @SystemApi
      @RequiresPermission(android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
      public void unblockContent(TvContentRating unblockedRating) {
          if (mSession != null) {
              mSession.unblockContent(unblockedRating);
          }
      }
  
      /**
       * Enables or disables the caption in this TvView.
       *
       * <p>Note that this method does not take any effect unless the current TvView is tuned.
       *
       * @param enabled {@code true} to enable, {@code false} to disable.
       */
      public void setCaptionEnabled(boolean enabled) {
          if (DEBUG) Log.d(TAG, "setCaptionEnabled(" + enabled + ")");
          mCaptionEnabled = enabled;
          if (mSession != null) {
              mSession.setCaptionEnabled(enabled);
          }
      }
  
      /**
       * 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. {@code null} means to unselect the current
       *            track for a given type.
       * @see #getTracks
       * @see #getSelectedTrack
       */
      public void selectTrack(int type, String trackId) {
          if (mSession != null) {
              mSession.selectTrack(type, trackId);
          }
      }
  
      /**
       * Returns the list of tracks. 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}.
       * @see #selectTrack
       * @see #getSelectedTrack
       */
      public List<TvTrackInfo> getTracks(int type) {
          if (mSession == null) {
              return null;
          }
          return mSession.getTracks(type);
      }
  
      /**
       * Returns the ID of the selected track for a given type. Returns {@code null} if the
       * information is not available or the track is not selected.
       *
       * @param type The type of the selected tracks. The type can be {@link TvTrackInfo#TYPE_AUDIO},
       *            {@link TvTrackInfo#TYPE_VIDEO} or {@link TvTrackInfo#TYPE_SUBTITLE}.
       * @see #selectTrack
       * @see #getTracks
       */
      public String getSelectedTrack(int type) {
          if (mSession == null) {
              return null;
          }
          return mSession.getSelectedTrack(type);
      }
  
      /**
       * Plays a given recorded TV program.
       *
       * @param inputId The ID of the TV input that created the given recorded program.
       * @param recordedProgramUri The URI of a recorded program.
       */
      public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
          if (DEBUG) Log.d(TAG, "timeShiftPlay(" + recordedProgramUri + ")");
          if (TextUtils.isEmpty(inputId)) {
              throw new IllegalArgumentException("inputId cannot be null or an empty string");
          }
          synchronized (sMainTvViewLock) {
              if (sMainTvView.get() == null) {
                  sMainTvView = new WeakReference<>(this);
              }
          }
          if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) {
              if (mSession != null) {
                  mSession.timeShiftPlay(recordedProgramUri);
              } else {
                  mSessionCallback.mRecordedProgramUri = recordedProgramUri;
              }
          } else {
              resetInternal();
              mSessionCallback = new MySessionCallback(inputId, recordedProgramUri);
              if (mTvInputManager != null) {
                  mTvInputManager.createSession(inputId, mSessionCallback, mHandler);
              }
          }
      }
  
      /**
       * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume.
       */
      public void timeShiftPause() {
          if (mSession != null) {
              mSession.timeShiftPause();
          }
      }
  
      /**
       * Resumes playback. No-op if it is already resumed. Call {@link #timeShiftPause} to pause.
       */
      public void timeShiftResume() {
          if (mSession != null) {
              mSession.timeShiftResume();
          }
      }
  
      /**
       * Seeks to a specified time position. {@code timeMs} must be equal to or greater than the start
       * position returned by {@link TimeShiftPositionCallback#onTimeShiftStartPositionChanged} and
       * equal to or less than the current time.
       *
       * @param timeMs The time position to seek to, in milliseconds since the epoch.
       */
      public void timeShiftSeekTo(long timeMs) {
          if (mSession != null) {
              mSession.timeShiftSeekTo(timeMs);
          }
      }
  
      /**
       * Sets playback rate using {@link android.media.PlaybackParams}.
       *
       * @param params The playback params.
       */
      public void timeShiftSetPlaybackParams(@NonNull PlaybackParams params) {
          if (mSession != null) {
              mSession.timeShiftSetPlaybackParams(params);
          }
      }
  
      /**
       * Sets the callback to be invoked when the time shift position is changed.
       *
       * @param callback The callback to receive time shift position changes. A value of {@code null}
       *            removes the existing callback.
       */
      public void setTimeShiftPositionCallback(@Nullable TimeShiftPositionCallback callback) {
          mTimeShiftPositionCallback = callback;
          ensurePositionTracking();
      }
  
      private void ensurePositionTracking() {
          if (mSession == null) {
              return;
          }
          mSession.timeShiftEnablePositionTracking(mTimeShiftPositionCallback != null);
      }
  
      /**
       * Sends a private command to the underlying TV input. This can be used to provide
       * domain-specific features that are only known between certain clients and their TV inputs.
       *
       * @param action The name of the private command to send. 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 An optional bundle to send with the command.
       */
      public void sendAppPrivateCommand(@NonNull String action, Bundle data) {
          if (TextUtils.isEmpty(action)) {
              throw new IllegalArgumentException("action cannot be null or an empty string");
          }
          if (mSession != null) {
              mSession.sendAppPrivateCommand(action, data);
          } else {
              Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action
                      + "\" pending)");
              mPendingAppPrivateCommands.add(Pair.create(action, data));
          }
      }
  
      /**
       * Dispatches an unhandled input event to the next receiver.
       *
       * <p>Except system keys, TvView always consumes input events in the normal flow. This is called
       * asynchronously from where the event is dispatched. It gives the host application a chance to
       * dispatch the unhandled input events.
       *
       * @param event The input event.
       * @return {@code true} if the event was handled by the view, {@code false} otherwise.
       */
      public boolean dispatchUnhandledInputEvent(InputEvent event) {
          if (mOnUnhandledInputEventListener != null) {
              if (mOnUnhandledInputEventListener.onUnhandledInputEvent(event)) {
                  return true;
              }
          }
          return onUnhandledInputEvent(event);
      }
  
      /**
       * Called when an unhandled input event also has not been handled by the user provided
       * callback. This is the last chance to handle the unhandled input event in the TvView.
       *
       * @param event The input event.
       * @return If you handled the event, return {@code true}. If you want to allow the event to be
       *         handled by the next receiver, return {@code false}.
       */
      public boolean onUnhandledInputEvent(InputEvent event) {
          return false;
      }
  
      /**
       * Registers a callback to be invoked when an input event is not handled by the bound TV input.
       *
       * @param listener The callback to be invoked when the unhandled input event is received.
       */
      public void setOnUnhandledInputEventListener(OnUnhandledInputEventListener listener) {
          mOnUnhandledInputEventListener = listener;
      }
  
      @Override
      public boolean dispatchKeyEvent(KeyEvent event) {
          if (super.dispatchKeyEvent(event)) {
              return true;
          }
          if (DEBUG) Log.d(TAG, "dispatchKeyEvent(" + event + ")");
          if (mSession == null) {
              return false;
          }
          InputEvent copiedEvent = event.copy();
          int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
                  mHandler);
          return ret != Session.DISPATCH_NOT_HANDLED;
      }
  
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
          if (super.dispatchTouchEvent(event)) {
              return true;
          }
          if (DEBUG) Log.d(TAG, "dispatchTouchEvent(" + event + ")");
          if (mSession == null) {
              return false;
          }
          InputEvent copiedEvent = event.copy();
          int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
                  mHandler);
          return ret != Session.DISPATCH_NOT_HANDLED;
      }
  
      @Override
      public boolean dispatchTrackballEvent(MotionEvent event) {
          if (super.dispatchTrackballEvent(event)) {
              return true;
          }
          if (DEBUG) Log.d(TAG, "dispatchTrackballEvent(" + event + ")");
          if (mSession == null) {
              return false;
          }
          InputEvent copiedEvent = event.copy();
          int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
                  mHandler);
          return ret != Session.DISPATCH_NOT_HANDLED;
      }
  
      @Override
      public boolean dispatchGenericMotionEvent(MotionEvent event) {
          if (super.dispatchGenericMotionEvent(event)) {
              return true;
          }
          if (DEBUG) Log.d(TAG, "dispatchGenericMotionEvent(" + event + ")");
          if (mSession == null) {
              return false;
          }
          InputEvent copiedEvent = event.copy();
          int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, mFinishedInputEventCallback,
                  mHandler);
          return ret != Session.DISPATCH_NOT_HANDLED;
      }
  
      @Override
      public void dispatchWindowFocusChanged(boolean hasFocus) {
          super.dispatchWindowFocusChanged(hasFocus);
          // Other app may have shown its own main TvView.
          // Set main again to regain main session.
          synchronized (sMainTvViewLock) {
              if (hasFocus && this == sMainTvView.get() && mSession != null
                      && checkChangeHdmiCecActiveSourcePermission()) {
                  mSession.setMain();
              }
          }
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          createSessionOverlayView();
      }
  
      @Override
      protected void onDetachedFromWindow() {
          removeSessionOverlayView();
          super.onDetachedFromWindow();
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          if (DEBUG) {
              Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right
                      + ", bottom=" + bottom + ",)");
          }
          if (mUseRequestedSurfaceLayout) {
              mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight,
                      mSurfaceViewBottom);
          } else {
              mSurfaceView.layout(0, 0, right - left, bottom - top);
          }
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec);
          int width = mSurfaceView.getMeasuredWidth();
          int height = mSurfaceView.getMeasuredHeight();
          int childState = mSurfaceView.getMeasuredState();
          setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState),
                  resolveSizeAndState(height, heightMeasureSpec,
                          childState << MEASURED_HEIGHT_STATE_SHIFT));
      }
  
      @Override
      public boolean gatherTransparentRegion(Region region) {
          if (mWindowZOrder != ZORDER_ON_TOP) {
              if (region != null) {
                  int width = getWidth();
                  int height = getHeight();
                  if (width > 0 && height > 0) {
                      int location[] = new int[2];
                      getLocationInWindow(location);
                      int left = location[0];
                      int top = location[1];
                      region.op(left, top, left + width, top + height, Region.Op.UNION);
                  }
              }
          }
          return super.gatherTransparentRegion(region);
      }
  
      @Override
      public void draw(Canvas canvas) {
          if (mWindowZOrder != ZORDER_ON_TOP) {
              // Punch a hole so that the underlying overlay view and surface can be shown.
              canvas.drawColor(0, PorterDuff.Mode.CLEAR);
          }
          super.draw(canvas);
      }
  
      @Override
      protected void dispatchDraw(Canvas canvas) {
          if (mWindowZOrder != ZORDER_ON_TOP) {
              // Punch a hole so that the underlying overlay view and surface can be shown.
              canvas.drawColor(0, PorterDuff.Mode.CLEAR);
          }
          super.dispatchDraw(canvas);
      }
  
      @Override
      protected void onVisibilityChanged(View changedView, int visibility) {
          super.onVisibilityChanged(changedView, visibility);
          mSurfaceView.setVisibility(visibility);
          if (visibility == View.VISIBLE) {
              createSessionOverlayView();
          } else {
              removeSessionOverlayView();
          }
      }
  
      private void resetSurfaceView() {
          if (mSurfaceView != null) {
              mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback);
              removeView(mSurfaceView);
          }
          mSurface = null;
          mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) {
              @Override
              protected void updateSurface() {
                  super.updateSurface();
                  relayoutSessionOverlayView();
              }};
          // The surface view's content should be treated as secure all the time.
          mSurfaceView.setSecure(true);
          mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
          if (mWindowZOrder == ZORDER_MEDIA_OVERLAY) {
              mSurfaceView.setZOrderMediaOverlay(true);
          } else if (mWindowZOrder == ZORDER_ON_TOP) {
              mSurfaceView.setZOrderOnTop(true);
          }
          addView(mSurfaceView);
      }
  
      private void setSessionSurface(Surface surface) {
          if (mSession == null) {
              return;
          }
          mSession.setSurface(surface);
      }
  
      private void dispatchSurfaceChanged(int format, int width, int height) {
          if (mSession == null) {
              return;
          }
          mSession.dispatchSurfaceChanged(format, width, height);
      }
  
      private void createSessionOverlayView() {
          if (mSession == null || !isAttachedToWindow()
                  || mOverlayViewCreated || mWindowZOrder != ZORDER_MEDIA) {
              return;
          }
          mOverlayViewFrame = getViewFrameOnScreen();
          mSession.createOverlayView(this, mOverlayViewFrame);
          mOverlayViewCreated = true;
      }
  
      private void removeSessionOverlayView() {
          if (mSession == null || !mOverlayViewCreated) {
              return;
          }
          mSession.removeOverlayView();
          mOverlayViewCreated = false;
          mOverlayViewFrame = null;
      }
  
      private void relayoutSessionOverlayView() {
          if (mSession == null || !isAttachedToWindow() || !mOverlayViewCreated
                  || mWindowZOrder != ZORDER_MEDIA) {
              return;
          }
          Rect viewFrame = getViewFrameOnScreen();
          if (viewFrame.equals(mOverlayViewFrame)) {
              return;
          }
          mSession.relayoutOverlayView(viewFrame);
          mOverlayViewFrame = viewFrame;
      }
  
      private Rect getViewFrameOnScreen() {
          Rect frame = new Rect();
          getGlobalVisibleRect(frame);
          RectF frameF = new RectF(frame);
          getMatrix().mapRect(frameF);
          frameF.round(frame);
          return frame;
      }
  
      private boolean checkChangeHdmiCecActiveSourcePermission() {
          return getContext().checkSelfPermission(
                  android.Manifest.permission.CHANGE_HDMI_CEC_ACTIVE_SOURCE)
                          == PackageManager.PERMISSION_GRANTED;
      }
  
      /**
       * Callback used to receive time shift position changes.
       */
      public abstract static class TimeShiftPositionCallback {
  
          /**
           * This is called when the start position for time shifting has changed.
           *
           * <p>The start position for time shifting indicates the earliest possible time the user can
           * seek to. Initially this is equivalent to the time when the underlying TV input starts
           * recording. Later it may be adjusted because there is insufficient space or the duration
           * of recording is limited. The application must not allow the user to seek to a position
           * earlier than the start position.
           *
           * <p>For playback of a recorded program initiated by {@link #timeShiftPlay(String, Uri)},
           * the start position is the time when playback starts. It does not change.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param timeMs The start position for time shifting, in milliseconds since the epoch.
           */
          public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
          }
  
          /**
           * This is called when the current position for time shifting has changed.
           *
           * <p>The current position for time shifting is the same as the current position of
           * playback. During playback, the current position changes continuously. When paused, it
           * does not change.
           *
           * <p>Note that {@code timeMs} is wall-clock time.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param timeMs The current position for time shifting, in milliseconds since the epoch.
           */
          public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
          }
      }
  
      /**
       * Callback used to receive various status updates on the {@link TvView}.
       */
      public abstract static class TvInputCallback {
  
          /**
           * This is invoked when an error occurred while establishing a connection to the underlying
           * TV input.
           *
           * @param inputId The ID of the TV input bound to this view.
           */
          public void onConnectionFailed(String inputId) {
          }
  
          /**
           * This is invoked when the existing connection to the underlying TV input is lost.
           *
           * @param inputId The ID of the TV input bound to this view.
           */
          public void onDisconnected(String inputId) {
          }
  
          /**
           * This is invoked when the channel of this TvView is changed by the underlying TV input
           * without any {@link TvView#tune} request.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param channelUri The URI of a channel.
           */
          public void onChannelRetuned(String inputId, Uri channelUri) {
          }
  
          /**
           * This is called when the track information has been changed.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param tracks A list which includes track information.
           */
          public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
          }
  
          /**
           * This is called when there is a change on the selected tracks.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param type The type of the track selected. The type can be
           *            {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or
           *            {@link TvTrackInfo#TYPE_SUBTITLE}.
           * @param trackId The ID of the track selected.
           */
          public void onTrackSelected(String inputId, int type, 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 this view is tuned to a specific
           * channel.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param width The width of the video.
           * @param height The height of the video.
           */
          public void onVideoSizeChanged(String inputId, int width, int height) {
          }
  
          /**
           * This is called when the video is available, so the TV input starts the playback.
           *
           * @param inputId The ID of the TV input bound to this view.
           */
          public void onVideoAvailable(String inputId) {
          }
  
          /**
           * This is called when the video is not available, so the TV input stops the playback.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @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(
                  String inputId, @TvInputManager.VideoUnavailableReason 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 inputId The ID of the TV input bound to this view.
           */
          public void onContentAllowed(String inputId) {
          }
  
          /**
           * 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 inputId The ID of the TV input bound to this view.
           * @param rating The content rating of the blocked program.
           */
          public void onContentBlocked(String inputId, TvContentRating rating) {
          }
  
          /**
           * This is invoked when a custom event from the bound TV input is sent to this view.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @param eventType The type of the event.
           * @param eventArgs Optional arguments of the event.
           * @hide
           */
          @SystemApi
          public void onEvent(String inputId, String eventType, Bundle eventArgs) {
          }
  
          /**
           * This is called when the time shift status is changed.
           *
           * @param inputId The ID of the TV input bound to this view.
           * @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(
                  String inputId, @TvInputManager.TimeShiftStatus int status) {
          }
      }
  
      /**
       * Interface definition for a callback to be invoked when the unhandled input event is received.
       */
      public interface OnUnhandledInputEventListener {
          /**
           * Called when an input event was not handled by the bound TV input.
           *
           * <p>This is called asynchronously from where the event is dispatched. It gives the host
           * application a chance to handle the unhandled input events.
           *
           * @param event The input event.
           * @return If you handled the event, return {@code true}. If you want to allow the event to
           *         be handled by the next receiver, return {@code false}.
           */
          boolean onUnhandledInputEvent(InputEvent event);
      }
  
      private class MySessionCallback extends SessionCallback {
          final String mInputId;
          Uri mChannelUri;
          Bundle mTuneParams;
          Uri mRecordedProgramUri;
  
          MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
              mInputId = inputId;
              mChannelUri = channelUri;
              mTuneParams = tuneParams;
          }
  
          MySessionCallback(String inputId, Uri recordedProgramUri) {
              mInputId = inputId;
              mRecordedProgramUri = recordedProgramUri;
          }
  
          @Override
          public void onSessionCreated(Session session) {
              if (DEBUG) {
                  Log.d(TAG, "onSessionCreated()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onSessionCreated - session already created");
                  // This callback is obsolete.
                  if (session != null) {
                      session.release();
                  }
                  return;
              }
              mSession = session;
              if (session != null) {
                  // Sends the pending app private commands first.
                  for (Pair<String, Bundle> command : mPendingAppPrivateCommands) {
                      mSession.sendAppPrivateCommand(command.first, command.second);
                  }
                  mPendingAppPrivateCommands.clear();
  
                  synchronized (sMainTvViewLock) {
                      if (hasWindowFocus() && TvView.this == sMainTvView.get()
                              && checkChangeHdmiCecActiveSourcePermission()) {
                          mSession.setMain();
                      }
                  }
                  // mSurface may not be ready yet as soon as starting an application.
                  // In the case, we don't send Session.setSurface(null) unnecessarily.
                  // setSessionSurface will be called in surfaceCreated.
                  if (mSurface != null) {
                      setSessionSurface(mSurface);
                      if (mSurfaceChanged) {
                          dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
                      }
                  }
                  createSessionOverlayView();
                  if (mStreamVolume != null) {
                      mSession.setStreamVolume(mStreamVolume);
                  }
                  if (mCaptionEnabled != null) {
                      mSession.setCaptionEnabled(mCaptionEnabled);
                  }
                  if (mChannelUri != null) {
                      mSession.tune(mChannelUri, mTuneParams);
                  } else {
                      mSession.timeShiftPlay(mRecordedProgramUri);
                  }
                  ensurePositionTracking();
              } else {
                  mSessionCallback = null;
                  if (mCallback != null) {
                      mCallback.onConnectionFailed(mInputId);
                  }
              }
          }
  
          @Override
          public void onSessionReleased(Session session) {
              if (DEBUG) {
                  Log.d(TAG, "onSessionReleased()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onSessionReleased - session not created");
                  return;
              }
              mOverlayViewCreated = false;
              mOverlayViewFrame = null;
              mSessionCallback = null;
              mSession = null;
              if (mCallback != null) {
                  mCallback.onDisconnected(mInputId);
              }
          }
  
          @Override
          public void onChannelRetuned(Session session, Uri channelUri) {
              if (DEBUG) {
                  Log.d(TAG, "onChannelChangedByTvInput(" + channelUri + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onChannelRetuned - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onChannelRetuned(mInputId, channelUri);
              }
          }
  
          @Override
          public void onTracksChanged(Session session, List<TvTrackInfo> tracks) {
              if (DEBUG) {
                  Log.d(TAG, "onTracksChanged(" + tracks + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onTracksChanged - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onTracksChanged(mInputId, tracks);
              }
          }
  
          @Override
          public void onTrackSelected(Session session, int type, String trackId) {
              if (DEBUG) {
                  Log.d(TAG, "onTrackSelected(type=" + type + ", trackId=" + trackId + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onTrackSelected - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onTrackSelected(mInputId, type, trackId);
              }
          }
  
          @Override
          public void onVideoSizeChanged(Session session, int width, int height) {
              if (DEBUG) {
                  Log.d(TAG, "onVideoSizeChanged()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onVideoSizeChanged - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onVideoSizeChanged(mInputId, width, height);
              }
          }
  
          @Override
          public void onVideoAvailable(Session session) {
              if (DEBUG) {
                  Log.d(TAG, "onVideoAvailable()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onVideoAvailable - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onVideoAvailable(mInputId);
              }
          }
  
          @Override
          public void onVideoUnavailable(Session session, int reason) {
              if (DEBUG) {
                  Log.d(TAG, "onVideoUnavailable(reason=" + reason + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onVideoUnavailable - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onVideoUnavailable(mInputId, reason);
              }
          }
  
          @Override
          public void onContentAllowed(Session session) {
              if (DEBUG) {
                  Log.d(TAG, "onContentAllowed()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onContentAllowed - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onContentAllowed(mInputId);
              }
          }
  
          @Override
          public void onContentBlocked(Session session, TvContentRating rating) {
              if (DEBUG) {
                  Log.d(TAG, "onContentBlocked(rating=" + rating + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onContentBlocked - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onContentBlocked(mInputId, rating);
              }
          }
  
          @Override
          public void onLayoutSurface(Session session, int left, int top, int right, int bottom) {
              if (DEBUG) {
                  Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right="
                          + right + ", bottom=" + bottom + ",)");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onLayoutSurface - session not created");
                  return;
              }
              mSurfaceViewLeft = left;
              mSurfaceViewTop = top;
              mSurfaceViewRight = right;
              mSurfaceViewBottom = bottom;
              mUseRequestedSurfaceLayout = true;
              requestLayout();
          }
  
          @Override
          public void onSessionEvent(Session session, String eventType, Bundle eventArgs) {
              if (DEBUG) {
                  Log.d(TAG, "onSessionEvent(" + eventType + ")");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onSessionEvent - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onEvent(mInputId, eventType, eventArgs);
              }
          }
  
          @Override
          public void onTimeShiftStatusChanged(Session session, int status) {
              if (DEBUG) {
                  Log.d(TAG, "onTimeShiftStatusChanged()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onTimeShiftStatusChanged - session not created");
                  return;
              }
              if (mCallback != null) {
                  mCallback.onTimeShiftStatusChanged(mInputId, status);
              }
          }
  
          @Override
          public void onTimeShiftStartPositionChanged(Session session, long timeMs) {
              if (DEBUG) {
                  Log.d(TAG, "onTimeShiftStartPositionChanged()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onTimeShiftStartPositionChanged - session not created");
                  return;
              }
              if (mTimeShiftPositionCallback != null) {
                  mTimeShiftPositionCallback.onTimeShiftStartPositionChanged(mInputId, timeMs);
              }
          }
  
          @Override
          public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) {
              if (DEBUG) {
                  Log.d(TAG, "onTimeShiftCurrentPositionChanged()");
              }
              if (this != mSessionCallback) {
                  Log.w(TAG, "onTimeShiftCurrentPositionChanged - session not created");
                  return;
              }
              if (mTimeShiftPositionCallback != null) {
                  mTimeShiftPositionCallback.onTimeShiftCurrentPositionChanged(mInputId, timeMs);
              }
          }
      }
  }
  