/*
 * 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 TvInputService. (Note that the list of TV inputs available on the system can be obtained by calling TvInputManager.getTvInputList().)

Once the application supplies the URI for a specific TV channel to 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.

/** * 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.
Params:
  • callback – The callback to receive events. A value of null removes the existing callback.
/** * 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 TvView.

The main TvView is a 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.

First tuned TvView becomes main automatically, and keeps to be main until either reset is called for the main TvView or setMain() is called for other TvView.

@hide
/** * 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.

Note that this must be set before the TvView's containing window is attached to the window manager.

Calling this overrides any previous call to setZOrderOnTop.

Params:
  • isMediaOverlay – true to be on top of another regular surface, false otherwise.
/** * 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.

Note that this must be set before the TvView's containing window is attached to the window manager.

Calling this overrides any previous call to setZOrderMediaOverlay.

Params:
  • onTop – true to be on top of its window, false otherwise.
/** * 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.

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 1.0f.

Params:
  • volume – A volume value between 0.0f to 1.0f.
/** * 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.
Params:
  • inputId – The ID of the TV input for the given channel.
  • channelUri – The URI of a channel.
/** * 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.
Params:
  • inputId – The ID of TV input for the given channel.
  • channelUri – The URI of a channel.
  • params – Domain-specific data for this tune request. Keys must be a scoped name, i.e. prefixed with a package name you own, so that different developers will not create conflicting keys.
/** * 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.

This method is primarily used to un-tune the current TvView.

/** * 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.

This notifies TV input that blocked content is now OK to play.

Params:
  • unblockedRating – A TvContentRating to unblock.
See Also:
@removed
/** * 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.

This notifies TV input that blocked content is now OK to play.

Params:
  • unblockedRating – A TvContentRating to unblock.
See Also:
@hide
/** * 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.

Note that this method does not take any effect unless the current TvView is tuned.

Params:
  • enabled – true to enable, false to disable.
/** * 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.
Params:
See Also:
/** * 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 null if the information is not available.
Params:
See Also:
/** * 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 null if the information is not available or the track is not selected.
Params:
See Also:
/** * 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.
Params:
  • inputId – The ID of the TV input that created the given recorded program.
  • recordedProgramUri – The URI of a recorded program.
/** * 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 timeShiftResume to resume.
/** * 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 timeShiftPause to pause.
/** * 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. timeMs must be equal to or greater than the start position returned by TimeShiftPositionCallback.onTimeShiftStartPositionChanged and equal to or less than the current time.
Params:
  • timeMs – The time position to seek to, in milliseconds since the epoch.
/** * 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 PlaybackParams.
Params:
  • params – The playback params.
/** * 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.
Params:
  • callback – The callback to receive time shift position changes. A value of null removes the existing callback.
/** * 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.
Params:
  • action – The name of the private command to send. This must be a scoped name, i.e. prefixed with a package name you own, so that different developers will not create conflicting commands.
  • data – An optional bundle to send with the command.
/** * 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.

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.

Params:
  • event – The input event.
Returns:true if the event was handled by the view, false otherwise.
/** * 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.
Params:
  • event – The input event.
Returns:If you handled the event, return true. If you want to allow the event to be handled by the next receiver, return false.
/** * 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.
Params:
  • listener – The callback to be invoked when the unhandled input event is received.
/** * 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.
/** * 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.

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.

For playback of a recorded program initiated by TvView.timeShiftPlay(String, Uri), the start position is the time when playback starts. It does not change.

Params:
  • inputId – The ID of the TV input bound to this view.
  • timeMs – The start position for time shifting, in milliseconds since the epoch.
/** * 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.

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.

Note that timeMs is wall-clock time.

Params:
  • inputId – The ID of the TV input bound to this view.
  • timeMs – The current position for time shifting, in milliseconds since the epoch.
/** * 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 TvView.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
/** * 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 TvView.tune request.
Params:
  • inputId – The ID of the TV input bound to this view.
  • channelUri – The URI of a channel.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
  • tracks – A list which includes track information.
/** * 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.
Params:
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
  • width – The width of the video.
  • height – The height of the video.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
/** * 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.
Params:
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
  • rating – The content rating of the blocked program.
/** * 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.
Params:
  • inputId – The ID of the TV input bound to this view.
  • eventType – The type of the event.
  • eventArgs – Optional arguments of the event.
@hide
/** * 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.
Params:
/** * 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.
/** * 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.

This is called asynchronously from where the event is dispatched. It gives the host application a chance to handle the unhandled input events.

Params:
  • event – The input event.
Returns:If you handled the event, return true. If you want to allow the event to be handled by the next receiver, return false.
/** * 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); } } } }