/*
 * 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.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.Rect;
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(String, Uri)}
 * 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
    public void setMain() {
        synchronized (sMainTvViewLock) {
            sMainTvView = new WeakReference<>(this);
            if (hasWindowFocus() && mSession != null) {
                mSession.setMain();
            }
        }
    }

    /**
     * Sets the Z order of a window owning the surface of this TvView above the normal TvView
     * but below an application.
     *
     * @see SurfaceView#setZOrderMediaOverlay
     * @hide
     */
    @SystemApi
    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);
        }
    }

    /**
     * Sets the Z order of a window owning the surface of this TvView on top of an application.
     *
     * @see SurfaceView#setZOrderOnTop
     * @hide
     */
    @SystemApi
    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(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.
     *
     * @param inputId The ID of TV input for the given channel.
     * @param channelUri The URI of a channel.
     * @param params Extra parameters.
     * @hide
     */
    @SystemApi
    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)
     * @hide
     * @deprecated Use {@link #unblockContent} instead.
     */
    @Deprecated
    @SystemApi
    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
    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);
    }

    /**
     * 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);
    }

    /**
     * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle)
     * TvInputService.Session.appPrivateCommand()} on the current TvView.
     *
     * @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.
     * @hide
     */
    @SystemApi
    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) {
                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 updateWindow(boolean force, boolean redrawNeeded) {
                super.updateWindow(force, redrawNeeded);
                relayoutSessionOverlayView();
            }};
        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() {
        int[] location = new int[2];
        getLocationOnScreen(location);
        return new Rect(location[0], location[1],
                location[0] + getWidth(), location[1] + getHeight());
    }

    /**
     * Callback used to receive time shift position changes.
     */
    public abstract static class TimeShiftPositionCallback {

        /**
         * This is called when the start playback position is changed.
         *
         * <p>The start playback position of the time shifted program can be adjusted by the TV
         * input when it cannot retain the whole recorded program due to some reason (e.g.
         * limitation on storage space). The application should not allow the user to seek to a
         * position earlier than the start position.
         *
         * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time,
         * which is intended to avoid calling this method unnecessarily around program boundaries.
         *
         * @param inputId The ID of the TV input bound to this view.
         * @param timeMs The start playback position of the time shifted program, in milliseconds
         *            since the epoch.
         */
        public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
        }

        /**
         * This is called when the current playback position is changed.
         *
         * <p>Note that {@code timeMs} is not relative time in the program but wall-clock time,
         * which is intended to avoid calling this method unnecessarily around program boundaries.
         *
         * @param inputId The ID of the TV input bound to this view.
         * @param timeMs The current playback position of the time shifted program, 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(String, Uri)} 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, 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, 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;

        MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
            mInputId = inputId;
            mChannelUri = channelUri;
            mTuneParams = tuneParams;
        }

        @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()) {
                        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);
                }
                mSession.tune(mChannelUri, mTuneParams);
                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);
            }
        }
    }
}
