/* * 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.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; import android.graphics.Rect; import android.hardware.hdmi.HdmiDeviceInfo; import android.media.PlaybackParams; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.WindowManager; import android.view.accessibility.CaptioningManager; import android.widget.FrameLayout; import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The TvInputService class represents a TV input or source such as HDMI or built-in tuner which * provides pass-through video or broadcast TV programs. * *

Applications will not normally use this service themselves, instead relying on the standard * interaction provided by {@link TvView}. Those implementing TV input services should normally do * so by deriving from this class and providing their own session implementation based on * {@link TvInputService.Session}. All TV input services must require that clients hold the * {@link android.Manifest.permission#BIND_TV_INPUT} in order to interact with the service; if this * permission is not specified in the manifest, the system will refuse to bind to that TV input * service. */ public abstract class TvInputService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvInputService"; private static final int DETACH_OVERLAY_VIEW_TIMEOUT_MS = 5000; /** * This is the interface name that a service implementing a TV input should say that it support * -- that is, this is the action it uses for its intent filter. To be supported, the service * must also require the {@link android.Manifest.permission#BIND_TV_INPUT} permission so that * other applications cannot abuse it. */ public static final String SERVICE_INTERFACE = "android.media.tv.TvInputService"; /** * Name under which a TvInputService component publishes information about itself. * This meta-data must reference an XML resource containing an * <{@link android.R.styleable#TvInputService tv-input}> * tag. */ public static final String SERVICE_META_DATA = "android.media.tv.input"; /** * Handler instance to handle request from TV Input Manager Service. Should be run in the main * looper to be synchronously run with {@code Session.mHandler}. */ private final Handler mServiceHandler = new ServiceHandler(); private final RemoteCallbackList mCallbacks = new RemoteCallbackList<>(); private TvInputManager mTvInputManager; @Override public final IBinder onBind(Intent intent) { return new ITvInputService.Stub() { @Override public void registerCallback(ITvInputServiceCallback cb) { if (cb != null) { mCallbacks.register(cb); } } @Override public void unregisterCallback(ITvInputServiceCallback cb) { if (cb != null) { mCallbacks.unregister(cb); } } @Override public void createSession(InputChannel channel, ITvInputSessionCallback cb, String inputId) { if (channel == null) { Log.w(TAG, "Creating session without input channel"); } if (cb == null) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = channel; args.arg2 = cb; args.arg3 = inputId; mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args).sendToTarget(); } @Override public void createRecordingSession(ITvInputSessionCallback cb, String inputId) { if (cb == null) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = cb; args.arg2 = inputId; mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_RECORDING_SESSION, args) .sendToTarget(); } @Override public void notifyHardwareAdded(TvInputHardwareInfo hardwareInfo) { mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HARDWARE_INPUT, hardwareInfo).sendToTarget(); } @Override public void notifyHardwareRemoved(TvInputHardwareInfo hardwareInfo) { mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HARDWARE_INPUT, hardwareInfo).sendToTarget(); } @Override public void notifyHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) { mServiceHandler.obtainMessage(ServiceHandler.DO_ADD_HDMI_INPUT, deviceInfo).sendToTarget(); } @Override public void notifyHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { mServiceHandler.obtainMessage(ServiceHandler.DO_REMOVE_HDMI_INPUT, deviceInfo).sendToTarget(); } }; } /** * Returns a concrete implementation of {@link Session}. * *

May return {@code null} if this TV input service fails to create a session for some * reason. If TV input represents an external device connected to a hardware TV input, * {@link HardwareSession} should be returned. * * @param inputId The ID of the TV input associated with the session. */ @Nullable public abstract Session onCreateSession(String inputId); /** * Returns a concrete implementation of {@link RecordingSession}. * *

May return {@code null} if this TV input service fails to create a recording session for * some reason. * * @param inputId The ID of the TV input associated with the recording session. */ @Nullable public RecordingSession onCreateRecordingSession(String inputId) { return null; } /** * Returns a new {@link TvInputInfo} object if this service is responsible for * {@code hardwareInfo}; otherwise, return {@code null}. Override to modify default behavior of * ignoring all hardware input. * * @param hardwareInfo {@link TvInputHardwareInfo} object just added. * @hide */ @Nullable @SystemApi public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo) { return null; } /** * Returns the input ID for {@code deviceId} if it is handled by this service; * otherwise, return {@code null}. Override to modify default behavior of ignoring all hardware * input. * * @param hardwareInfo {@link TvInputHardwareInfo} object just removed. * @hide */ @Nullable @SystemApi public String onHardwareRemoved(TvInputHardwareInfo hardwareInfo) { return null; } /** * Returns a new {@link TvInputInfo} object if this service is responsible for * {@code deviceInfo}; otherwise, return {@code null}. Override to modify default behavior of * ignoring all HDMI logical input device. * * @param deviceInfo {@link HdmiDeviceInfo} object just added. * @hide */ @Nullable @SystemApi public TvInputInfo onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) { return null; } /** * Returns the input ID for {@code deviceInfo} if it is handled by this service; otherwise, * return {@code null}. Override to modify default behavior of ignoring all HDMI logical input * device. * * @param deviceInfo {@link HdmiDeviceInfo} object just removed. * @hide */ @Nullable @SystemApi public String onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { return null; } private boolean isPassthroughInput(String inputId) { if (mTvInputManager == null) { mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); } TvInputInfo info = mTvInputManager.getTvInputInfo(inputId); return info != null && info.isPassthroughInput(); } /** * Base class for derived classes to implement to provide a TV input session. */ public abstract static class Session implements KeyEvent.Callback { private static final int POSITION_UPDATE_INTERVAL_MS = 1000; private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); private final WindowManager mWindowManager; final Handler mHandler; private WindowManager.LayoutParams mWindowParams; private Surface mSurface; private final Context mContext; private FrameLayout mOverlayViewContainer; private View mOverlayView; private OverlayViewCleanUpTask mOverlayViewCleanUpTask; private boolean mOverlayViewEnabled; private IBinder mWindowToken; private Rect mOverlayFrame; private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; private long mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; private final TimeShiftPositionTrackingRunnable mTimeShiftPositionTrackingRunnable = new TimeShiftPositionTrackingRunnable(); private final Object mLock = new Object(); // @GuardedBy("mLock") private ITvInputSessionCallback mSessionCallback; // @GuardedBy("mLock") private final List mPendingActions = new ArrayList<>(); /** * Creates a new Session. * * @param context The context of the application */ public Session(Context context) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mHandler = new Handler(context.getMainLooper()); } /** * Enables or disables the overlay view. * *

By default, the overlay view is disabled. Must be called explicitly after the * session is created to enable the overlay view. * *

The TV input service can disable its overlay view when the size of the overlay view is * insufficient to display the whole information, such as when used in Picture-in-picture. * Override {@link #onOverlayViewSizeChanged} to get the size of the overlay view, which * then can be used to determine whether to enable/disable the overlay view. * * @param enable {@code true} if you want to enable the overlay view. {@code false} * otherwise. */ public void setOverlayViewEnabled(final boolean enable) { mHandler.post(new Runnable() { @Override public void run() { if (enable == mOverlayViewEnabled) { return; } mOverlayViewEnabled = enable; if (enable) { if (mWindowToken != null) { createOverlayView(mWindowToken, mOverlayFrame); } } else { removeOverlayView(false); } } }); } /** * Dispatches an event to the application using this session. * * @param eventType The type of the event. * @param eventArgs Optional arguments of the event. * @hide */ @SystemApi public void notifySessionEvent(@NonNull final String eventType, final Bundle eventArgs) { Preconditions.checkNotNull(eventType); executeOrPostRunnableOnMainThread(new Runnable() { @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifySessionEvent(" + eventType + ")"); if (mSessionCallback != null) { mSessionCallback.onSessionEvent(eventType, eventArgs); } } catch (RemoteException e) { Log.w(TAG, "error in sending event (event=" + eventType + ")", e); } } }); } /** * Informs the application that the current channel is re-tuned for some reason and the * session now displays the content from a new channel. This is used to handle special cases * such as when the current channel becomes unavailable, it is necessary to send the user to * a certain channel or the user changes channel in some other way (e.g. by using a * dedicated remote). * * @param channelUri The URI of the new channel. */ public void notifyChannelRetuned(final Uri channelUri) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyChannelRetuned"); if (mSessionCallback != null) { mSessionCallback.onChannelRetuned(channelUri); } } catch (RemoteException e) { Log.w(TAG, "error in notifyChannelRetuned", e); } } }); } /** * Sends the list of all audio/video/subtitle tracks. The is used by the framework to * maintain the track information for a given session, which in turn is used by * {@link TvView#getTracks} for the application to retrieve metadata for a given track type. * The TV input service must call this method as soon as the track information becomes * available or is updated. Note that in a case where a part of the information for a * certain track is updated, it is not necessary to create a new {@link TvTrackInfo} object * with a different track ID. * * @param tracks A list which includes track information. */ public void notifyTracksChanged(final List tracks) { final List tracksCopy = new ArrayList<>(tracks); executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyTracksChanged"); if (mSessionCallback != null) { mSessionCallback.onTracksChanged(tracksCopy); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTracksChanged", e); } } }); } /** * Sends the type and ID of a selected track. This is used to inform the application that a * specific track is selected. The TV input service must call this method as soon as a track * is selected either by default or in response to a call to {@link #onSelectTrack}. The * selected track ID for a given type is maintained in the framework until the next call to * this method even after the entire track list is updated (but is reset when the session is * tuned to a new channel), so care must be taken not to result in an obsolete track ID. * * @param type The type of the selected track. The type can be * {@link TvTrackInfo#TYPE_AUDIO}, {@link TvTrackInfo#TYPE_VIDEO} or * {@link TvTrackInfo#TYPE_SUBTITLE}. * @param trackId The ID of the selected track. * @see #onSelectTrack */ public void notifyTrackSelected(final int type, final String trackId) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyTrackSelected"); if (mSessionCallback != null) { mSessionCallback.onTrackSelected(type, trackId); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTrackSelected", e); } } }); } /** * Informs the application that the video is now available for watching. Video is blocked * until this method is called. * *

The TV input service must call this method as soon as the content rendered onto its * surface is ready for viewing. This method must be called each time {@link #onTune} * is called. * * @see #notifyVideoUnavailable */ public void notifyVideoAvailable() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyVideoAvailable"); if (mSessionCallback != null) { mSessionCallback.onVideoAvailable(); } } catch (RemoteException e) { Log.w(TAG, "error in notifyVideoAvailable", e); } } }); } /** * Informs the application that the video became unavailable for some reason. This is * primarily used to signal the application to block the screen not to show any intermittent * video artifacts. * * @param reason The reason why the video became unavailable: *

* @see #notifyVideoAvailable */ public void notifyVideoUnavailable( @TvInputManager.VideoUnavailableReason final int reason) { if (reason < TvInputManager.VIDEO_UNAVAILABLE_REASON_START || reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) { Log.e(TAG, "notifyVideoUnavailable - unknown reason: " + reason); } executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyVideoUnavailable"); if (mSessionCallback != null) { mSessionCallback.onVideoUnavailable(reason); } } catch (RemoteException e) { Log.w(TAG, "error in notifyVideoUnavailable", e); } } }); } /** * Informs the application that the user is allowed to watch the current program content. * *

Each TV input service is required to query the system whether the user is allowed to * watch the current program before showing it to the user if the parental controls is * enabled (i.e. {@link TvInputManager#isParentalControlsEnabled * TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input * service should block the content or not is determined by invoking * {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)} * with the content rating for the current program. Then the {@link TvInputManager} makes a * judgment based on the user blocked ratings stored in the secure settings and returns the * result. If the rating in question turns out to be allowed by the user, the TV input * service must call this method to notify the application that is permitted to show the * content. * *

Each TV input service also needs to continuously listen to any changes made to the * parental controls settings by registering a broadcast receiver to receive * {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and * {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately * reevaluate the current program with the new parental controls settings. * * @see #notifyContentBlocked * @see TvInputManager */ public void notifyContentAllowed() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyContentAllowed"); if (mSessionCallback != null) { mSessionCallback.onContentAllowed(); } } catch (RemoteException e) { Log.w(TAG, "error in notifyContentAllowed", e); } } }); } /** * Informs the application that the current program content is blocked by parent controls. * *

Each TV input service is required to query the system whether the user is allowed to * watch the current program before showing it to the user if the parental controls is * enabled (i.e. {@link TvInputManager#isParentalControlsEnabled * TvInputManager.isParentalControlsEnabled()} returns {@code true}). Whether the TV input * service should block the content or not is determined by invoking * {@link TvInputManager#isRatingBlocked TvInputManager.isRatingBlocked(TvContentRating)} * with the content rating for the current program or {@link TvContentRating#UNRATED} in * case the rating information is missing. Then the {@link TvInputManager} makes a judgment * based on the user blocked ratings stored in the secure settings and returns the result. * If the rating in question turns out to be blocked, the TV input service must immediately * block the content and call this method with the content rating of the current program to * prompt the PIN verification screen. * *

Each TV input service also needs to continuously listen to any changes made to the * parental controls settings by registering a broadcast receiver to receive * {@link TvInputManager#ACTION_BLOCKED_RATINGS_CHANGED} and * {@link TvInputManager#ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED} and immediately * reevaluate the current program with the new parental controls settings. * * @param rating The content rating for the current TV program. Can be * {@link TvContentRating#UNRATED}. * @see #notifyContentAllowed * @see TvInputManager */ public void notifyContentBlocked(@NonNull final TvContentRating rating) { Preconditions.checkNotNull(rating); executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyContentBlocked"); if (mSessionCallback != null) { mSessionCallback.onContentBlocked(rating.flattenToString()); } } catch (RemoteException e) { Log.w(TAG, "error in notifyContentBlocked", e); } } }); } /** * Informs the application that the time shift status is changed. * *

Prior to calling this method, the application assumes the status * {@link TvInputManager#TIME_SHIFT_STATUS_UNKNOWN}. Right after the session is created, it * is important to invoke the method with the status * {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} if the implementation does support * time shifting, or {@link TvInputManager#TIME_SHIFT_STATUS_UNSUPPORTED} otherwise. Failure * to notifying the current status change immediately might result in an undesirable * behavior in the application such as hiding the play controls. * *

If the status {@link TvInputManager#TIME_SHIFT_STATUS_AVAILABLE} is reported, the * application assumes it can pause/resume playback, seek to a specified time position and * set playback rate and audio mode. The implementation should override * {@link #onTimeShiftPause}, {@link #onTimeShiftResume}, {@link #onTimeShiftSeekTo}, * {@link #onTimeShiftGetStartPosition}, {@link #onTimeShiftGetCurrentPosition} and * {@link #onTimeShiftSetPlaybackParams}. * * @param status The current time shift status. Should be one of the followings. *

*/ public void notifyTimeShiftStatusChanged(@TvInputManager.TimeShiftStatus final int status) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { timeShiftEnablePositionTracking( status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE); try { if (DEBUG) Log.d(TAG, "notifyTimeShiftStatusChanged"); if (mSessionCallback != null) { mSessionCallback.onTimeShiftStatusChanged(status); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTimeShiftStatusChanged", e); } } }); } private void notifyTimeShiftStartPositionChanged(final long timeMs) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyTimeShiftStartPositionChanged"); if (mSessionCallback != null) { mSessionCallback.onTimeShiftStartPositionChanged(timeMs); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTimeShiftStartPositionChanged", e); } } }); } private void notifyTimeShiftCurrentPositionChanged(final long timeMs) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyTimeShiftCurrentPositionChanged"); if (mSessionCallback != null) { mSessionCallback.onTimeShiftCurrentPositionChanged(timeMs); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTimeShiftCurrentPositionChanged", e); } } }); } /** * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position * is relative to the overlay view that sits on top of this surface. * * @param left Left position in pixels, relative to the overlay view. * @param top Top position in pixels, relative to the overlay view. * @param right Right position in pixels, relative to the overlay view. * @param bottom Bottom position in pixels, relative to the overlay view. * @see #onOverlayViewSizeChanged */ public void layoutSurface(final int left, final int top, final int right, final int bottom) { if (left > right || top > bottom) { throw new IllegalArgumentException("Invalid parameter"); } executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + ", r=" + right + ", b=" + bottom + ",)"); if (mSessionCallback != null) { mSessionCallback.onLayoutSurface(left, top, right, bottom); } } catch (RemoteException e) { Log.w(TAG, "error in layoutSurface", e); } } }); } /** * Called when the session is released. */ public abstract void onRelease(); /** * Sets the current session as the main session. The main session is a session whose * corresponding TV input determines the HDMI-CEC active source device. * *

TV input service that manages HDMI-CEC logical device should implement {@link * #onSetMain} to (1) select the corresponding HDMI logical device as the source device * when {@code isMain} is {@code true}, and to (2) select the internal device (= TV itself) * as the source device when {@code isMain} is {@code false} and the session is still main. * Also, if a surface is passed to a non-main session and active source is changed to * initiate the surface, the active source should be returned to the main session. * *

{@link TvView} guarantees that, when tuning involves a session transition, {@code * onSetMain(true)} for new session is called first, {@code onSetMain(false)} for old * session is called afterwards. This allows {@code onSetMain(false)} to be no-op when TV * input service knows that the next main session corresponds to another HDMI logical * device. Practically, this implies that one TV input service should handle all HDMI port * and HDMI-CEC logical devices for smooth active source transition. * * @param isMain If true, session should become main. * @see TvView#setMain * @hide */ @SystemApi public void onSetMain(boolean isMain) { } /** * Called when the application sets the surface. * *

The TV input service should render video onto the given surface. When called with * {@code null}, the input service should immediately free any references to the * currently set surface and stop using it. * * @param surface The surface to be used for video rendering. Can be {@code null}. * @return {@code true} if the surface was set successfully, {@code false} otherwise. */ public abstract boolean onSetSurface(@Nullable Surface surface); /** * Called after any structural changes (format or size) have been made to the surface passed * in {@link #onSetSurface}. This method is always called at least once, after * {@link #onSetSurface} is called with non-null surface. * * @param format The new PixelFormat of the surface. * @param width The new width of the surface. * @param height The new height of the surface. */ public void onSurfaceChanged(int format, int width, int height) { } /** * Called when the size of the overlay view is changed by the application. * *

This is always called at least once when the session is created regardless of whether * the overlay view is enabled or not. The overlay view size is the same as the containing * {@link TvView}. Note that the size of the underlying surface can be different if the * surface was changed by calling {@link #layoutSurface}. * * @param width The width of the overlay view. * @param height The height of the overlay view. */ public void onOverlayViewSizeChanged(int width, int height) { } /** * Sets the relative stream volume of the current TV input session. * *

The implementation should honor this request in order to handle audio focus changes or * mute the current session when multiple sessions, possibly from different inputs are * active. If the method has not yet been called, the implementation should assume the * default value of {@code 1.0f}. * * @param volume A volume value between {@code 0.0f} to {@code 1.0f}. */ public abstract void onSetStreamVolume(@FloatRange(from = 0.0, to = 1.0) float volume); /** * Tunes to a given channel. * *

No video will be displayed until {@link #notifyVideoAvailable()} is called. * Also, {@link #notifyVideoUnavailable(int)} should be called when the TV input cannot * continue playing the given channel. * * @param channelUri The URI of the channel. * @return {@code true} if the tuning was successful, {@code false} otherwise. */ public abstract boolean onTune(Uri channelUri); /** * Tunes to a given channel. Override this method in order to handle domain-specific * features that are only known between certain TV inputs and their clients. * *

The default implementation calls {@link #onTune(Uri)}. * * @param channelUri The URI of the channel. * @param 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. * @return {@code true} if the tuning was successful, {@code false} otherwise. */ public boolean onTune(Uri channelUri, Bundle params) { return onTune(channelUri); } /** * Enables or disables the caption. * *

The locale for the user's preferred captioning language can be obtained by calling * {@link CaptioningManager#getLocale CaptioningManager.getLocale()}. * * @param enabled {@code true} to enable, {@code false} to disable. * @see CaptioningManager */ public abstract void onSetCaptionEnabled(boolean enabled); /** * Requests to unblock the content according to the given rating. * *

The implementation should unblock the content. * TV input service has responsibility to decide when/how the unblock expires * while it can keep previously unblocked ratings in order not to ask a user * to unblock whenever a content rating is changed. * Therefore an unblocked rating can be valid for a channel, a program, * or certain amount of time depending on the implementation. * * @param unblockedRating An unblocked content rating */ public void onUnblockContent(TvContentRating unblockedRating) { } /** * Selects a given track. * *

If this is done successfully, the implementation should call * {@link #notifyTrackSelected} to help applications maintain the up-to-date list of the * selected tracks. * * @param trackId The ID of the track to select. {@code null} means to unselect the current * track for a given type. * @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}. * @return {@code true} if the track selection was successful, {@code false} otherwise. * @see #notifyTrackSelected */ public boolean onSelectTrack(int type, @Nullable String trackId) { return false; } /** * Processes a private command sent from the application to the TV input. This can be used * to provide domain-specific features that are only known between certain TV inputs and * their clients. * * @param action Name of the command to be performed. This must be a scoped name, * i.e. prefixed with a package name you own, so that different developers will * not create conflicting commands. * @param data Any data to include with the command. */ public void onAppPrivateCommand(@NonNull String action, Bundle data) { } /** * Called when the application requests to create an overlay view. Each session * implementation can override this method and return its own view. * * @return a view attached to the overlay window */ public View onCreateOverlayView() { return null; } /** * Called when the application requests to play a given recorded TV program. * * @param recordedProgramUri The URI of a recorded TV program. * @see #onTimeShiftResume() * @see #onTimeShiftPause() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetStartPosition() * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftPlay(Uri recordedProgramUri) { } /** * Called when the application requests to pause playback. * * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftResume() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetStartPosition() * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftPause() { } /** * Called when the application requests to resume playback. * * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftPause() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetStartPosition() * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftResume() { } /** * Called when the application requests to seek to a specified time position. Normally, the * position is given within range between the start and the current time, inclusively. The * implementation is expected to seek to the nearest time position if the given position is * not in the range. * * @param timeMs The time position to seek to, in milliseconds since the epoch. * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftResume() * @see #onTimeShiftPause() * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetStartPosition() * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftSeekTo(long timeMs) { } /** * Called when the application sets playback parameters containing the speed and audio mode. * *

Once the playback parameters are set, the implementation should honor the current * settings until the next tune request. Pause/resume/seek request does not reset the * parameters previously set. * * @param params The playback params. * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftResume() * @see #onTimeShiftPause() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftGetStartPosition() * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftSetPlaybackParams(PlaybackParams params) { } /** * Returns the start position for time shifting, in milliseconds since the epoch. * Returns {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if the position is unknown at the * moment. * *

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 implementation starts * recording. Later it may be adjusted because there is insufficient space or the duration * of recording is limited by the implementation. The application does not allow the user to * seek to a position earlier than the start position. * *

For playback of a recorded program initiated by {@link #onTimeShiftPlay(Uri)}, the * start position is the time when playback starts. It does not change. * * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftResume() * @see #onTimeShiftPause() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetCurrentPosition() */ public long onTimeShiftGetStartPosition() { return TvInputManager.TIME_SHIFT_INVALID_TIME; } /** * Returns the current position for time shifting, in milliseconds since the epoch. * Returns {@link TvInputManager#TIME_SHIFT_INVALID_TIME} if the position is unknown at the * moment. * *

The current position for time shifting is the same as the current position of * playback. It should be equal to or greater than the start position reported by * {@link #onTimeShiftGetStartPosition()}. When playback is completed, the current position * should stay where the playback ends, in other words, the returned value of this mehtod * should be equal to the start position plus the duration of the program. * * @see #onTimeShiftPlay(Uri) * @see #onTimeShiftResume() * @see #onTimeShiftPause() * @see #onTimeShiftSeekTo(long) * @see #onTimeShiftSetPlaybackParams(PlaybackParams) * @see #onTimeShiftGetStartPosition() */ public long onTimeShiftGetCurrentPosition() { return TvInputManager.TIME_SHIFT_INVALID_TIME; } /** * Default implementation of {@link android.view.KeyEvent.Callback#onKeyDown(int, KeyEvent) * KeyEvent.Callback.onKeyDown()}: always returns false (doesn't handle the event). * *

Override this to intercept key down events before they are processed by the * application. If you return true, the application will not process the event itself. If * you return false, the normal application processing will occur as if the TV input had not * seen the event at all. * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key 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}. */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } /** * Default implementation of * {@link android.view.KeyEvent.Callback#onKeyLongPress(int, KeyEvent) * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle the event). * *

Override this to intercept key long press events before they are processed by the * application. If you return true, the application will not process the event itself. If * you return false, the normal application processing will occur as if the TV input had not * seen the event at all. * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key 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}. */ @Override public boolean onKeyLongPress(int keyCode, KeyEvent event) { return false; } /** * Default implementation of * {@link android.view.KeyEvent.Callback#onKeyMultiple(int, int, KeyEvent) * KeyEvent.Callback.onKeyMultiple()}: always returns false (doesn't handle the event). * *

Override this to intercept special key multiple events before they are processed by * the application. If you return true, the application will not itself process the event. * If you return false, the normal application processing will occur as if the TV input had * not seen the event at all. * * @param keyCode The value in event.getKeyCode(). * @param count The number of times the action was made. * @param event Description of the key 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}. */ @Override public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { return false; } /** * Default implementation of {@link android.view.KeyEvent.Callback#onKeyUp(int, KeyEvent) * KeyEvent.Callback.onKeyUp()}: always returns false (doesn't handle the event). * *

Override this to intercept key up events before they are processed by the application. * If you return true, the application will not itself process the event. If you return false, * the normal application processing will occur as if the TV input had not seen the event at * all. * * @param keyCode The value in event.getKeyCode(). * @param event Description of the key 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}. */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { return false; } /** * Implement this method to handle touch screen motion events on the current input session. * * @param event The motion event being received. * @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}. * @see View#onTouchEvent */ public boolean onTouchEvent(MotionEvent event) { return false; } /** * Implement this method to handle trackball events on the current input session. * * @param event The motion event being received. * @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}. * @see View#onTrackballEvent */ public boolean onTrackballEvent(MotionEvent event) { return false; } /** * Implement this method to handle generic motion events on the current input session. * * @param event The motion event being received. * @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}. * @see View#onGenericMotionEvent */ public boolean onGenericMotionEvent(MotionEvent event) { return false; } /** * This method is called when the application would like to stop using the current input * session. */ void release() { onRelease(); if (mSurface != null) { mSurface.release(); mSurface = null; } synchronized(mLock) { mSessionCallback = null; mPendingActions.clear(); } // Removes the overlay view lastly so that any hanging on the main thread can be handled // in {@link #scheduleOverlayViewCleanup}. removeOverlayView(true); mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable); } /** * Calls {@link #onSetMain}. */ void setMain(boolean isMain) { onSetMain(isMain); } /** * Calls {@link #onSetSurface}. */ void setSurface(Surface surface) { onSetSurface(surface); if (mSurface != null) { mSurface.release(); } mSurface = surface; // TODO: Handle failure. } /** * Calls {@link #onSurfaceChanged}. */ void dispatchSurfaceChanged(int format, int width, int height) { if (DEBUG) { Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width + ", height=" + height + ")"); } onSurfaceChanged(format, width, height); } /** * Calls {@link #onSetStreamVolume}. */ void setStreamVolume(float volume) { onSetStreamVolume(volume); } /** * Calls {@link #onTune(Uri, Bundle)}. */ void tune(Uri channelUri, Bundle params) { mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; onTune(channelUri, params); // TODO: Handle failure. } /** * Calls {@link #onSetCaptionEnabled}. */ void setCaptionEnabled(boolean enabled) { onSetCaptionEnabled(enabled); } /** * Calls {@link #onSelectTrack}. */ void selectTrack(int type, String trackId) { onSelectTrack(type, trackId); } /** * Calls {@link #onUnblockContent}. */ void unblockContent(String unblockedRating) { onUnblockContent(TvContentRating.unflattenFromString(unblockedRating)); // TODO: Handle failure. } /** * Calls {@link #onAppPrivateCommand}. */ void appPrivateCommand(String action, Bundle data) { onAppPrivateCommand(action, data); } /** * Creates an overlay view. This calls {@link #onCreateOverlayView} to get a view to attach * to the overlay window. * * @param windowToken A window token of the application. * @param frame A position of the overlay view. */ void createOverlayView(IBinder windowToken, Rect frame) { if (mOverlayViewContainer != null) { removeOverlayView(false); } if (DEBUG) Log.d(TAG, "create overlay view(" + frame + ")"); mWindowToken = windowToken; mOverlayFrame = frame; onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); if (!mOverlayViewEnabled) { return; } mOverlayView = onCreateOverlayView(); if (mOverlayView == null) { return; } if (mOverlayViewCleanUpTask != null) { mOverlayViewCleanUpTask.cancel(true); mOverlayViewCleanUpTask = null; } // Creates a container view to check hanging on the overlay view detaching. // Adding/removing the overlay view to/from the container make the view attach/detach // logic run on the main thread. mOverlayViewContainer = new FrameLayout(mContext.getApplicationContext()); mOverlayViewContainer.addView(mOverlayView); // TvView's window type is TYPE_APPLICATION_MEDIA and we want to create // an overlay window above the media window but below the application window. int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY; // We make the overlay view non-focusable and non-touchable so that // the application that owns the window token can decide whether to consume or // dispatch the input events. int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; if (ActivityManager.isHighEndGfx()) { flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; } mWindowParams = new WindowManager.LayoutParams( frame.right - frame.left, frame.bottom - frame.top, frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT); mWindowParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; mWindowParams.gravity = Gravity.START | Gravity.TOP; mWindowParams.token = windowToken; mWindowManager.addView(mOverlayViewContainer, mWindowParams); } /** * Relayouts the current overlay view. * * @param frame A new position of the overlay view. */ void relayoutOverlayView(Rect frame) { if (DEBUG) Log.d(TAG, "relayoutOverlayView(" + frame + ")"); if (mOverlayFrame == null || mOverlayFrame.width() != frame.width() || mOverlayFrame.height() != frame.height()) { // Note: relayoutOverlayView is called whenever TvView's layout is changed // regardless of setOverlayViewEnabled. onOverlayViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); } mOverlayFrame = frame; if (!mOverlayViewEnabled || mOverlayViewContainer == null) { return; } mWindowParams.x = frame.left; mWindowParams.y = frame.top; mWindowParams.width = frame.right - frame.left; mWindowParams.height = frame.bottom - frame.top; mWindowManager.updateViewLayout(mOverlayViewContainer, mWindowParams); } /** * Removes the current overlay view. */ void removeOverlayView(boolean clearWindowToken) { if (DEBUG) Log.d(TAG, "removeOverlayView(" + mOverlayViewContainer + ")"); if (clearWindowToken) { mWindowToken = null; mOverlayFrame = null; } if (mOverlayViewContainer != null) { // Removes the overlay view from the view hierarchy in advance so that it can be // cleaned up in the {@link OverlayViewCleanUpTask} if the remove process is // hanging. mOverlayViewContainer.removeView(mOverlayView); mOverlayView = null; mWindowManager.removeView(mOverlayViewContainer); mOverlayViewContainer = null; mWindowParams = null; } } /** * Calls {@link #onTimeShiftPlay(Uri)}. */ void timeShiftPlay(Uri recordedProgramUri) { mCurrentPositionMs = 0; onTimeShiftPlay(recordedProgramUri); } /** * Calls {@link #onTimeShiftPause}. */ void timeShiftPause() { onTimeShiftPause(); } /** * Calls {@link #onTimeShiftResume}. */ void timeShiftResume() { onTimeShiftResume(); } /** * Calls {@link #onTimeShiftSeekTo}. */ void timeShiftSeekTo(long timeMs) { onTimeShiftSeekTo(timeMs); } /** * Calls {@link #onTimeShiftSetPlaybackParams}. */ void timeShiftSetPlaybackParams(PlaybackParams params) { onTimeShiftSetPlaybackParams(params); } /** * Enable/disable position tracking. * * @param enable {@code true} to enable tracking, {@code false} otherwise. */ void timeShiftEnablePositionTracking(boolean enable) { if (enable) { mHandler.post(mTimeShiftPositionTrackingRunnable); } else { mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable); mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; mCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; } } /** * Schedules a task which checks whether the overlay view is detached and kills the process * if it is not. Note that this method is expected to be called in a non-main thread. */ void scheduleOverlayViewCleanup() { View overlayViewParent = mOverlayViewContainer; if (overlayViewParent != null) { mOverlayViewCleanUpTask = new OverlayViewCleanUpTask(); mOverlayViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, overlayViewParent); } } /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); boolean isNavigationKey = false; boolean skipDispatchToOverlayView = false; if (event instanceof KeyEvent) { KeyEvent keyEvent = (KeyEvent) event; if (keyEvent.dispatch(this, mDispatcherState, this)) { return TvInputManager.Session.DISPATCH_HANDLED; } isNavigationKey = isNavigationKey(keyEvent.getKeyCode()); // When media keys and KEYCODE_MEDIA_AUDIO_TRACK are dispatched to ViewRootImpl, // ViewRootImpl always consumes the keys. In this case, the application loses // a chance to handle media keys. Therefore, media keys are not dispatched to // ViewRootImpl. skipDispatchToOverlayView = KeyEvent.isMediaKey(keyEvent.getKeyCode()) || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK; } else if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; final int source = motionEvent.getSource(); if (motionEvent.isTouchEvent()) { if (onTouchEvent(motionEvent)) { return TvInputManager.Session.DISPATCH_HANDLED; } } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { if (onTrackballEvent(motionEvent)) { return TvInputManager.Session.DISPATCH_HANDLED; } } else { if (onGenericMotionEvent(motionEvent)) { return TvInputManager.Session.DISPATCH_HANDLED; } } } if (mOverlayViewContainer == null || !mOverlayViewContainer.isAttachedToWindow() || skipDispatchToOverlayView) { return TvInputManager.Session.DISPATCH_NOT_HANDLED; } if (!mOverlayViewContainer.hasWindowFocus()) { mOverlayViewContainer.getViewRootImpl().windowFocusChanged(true, true); } if (isNavigationKey && mOverlayViewContainer.hasFocusable()) { // If mOverlayView has focusable views, navigation key events should be always // handled. If not, it can make the application UI navigation messed up. // For example, in the case that the left-most view is focused, a left key event // will not be handled in ViewRootImpl. Then, the left key event will be handled in // the application during the UI navigation of the TV input. mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event); return TvInputManager.Session.DISPATCH_HANDLED; } else { mOverlayViewContainer.getViewRootImpl().dispatchInputEvent(event, receiver); return TvInputManager.Session.DISPATCH_IN_PROGRESS; } } private void initialize(ITvInputSessionCallback callback) { synchronized(mLock) { mSessionCallback = callback; for (Runnable runnable : mPendingActions) { runnable.run(); } mPendingActions.clear(); } } private void executeOrPostRunnableOnMainThread(Runnable action) { synchronized(mLock) { if (mSessionCallback == null) { // The session is not initialized yet. mPendingActions.add(action); } else { if (mHandler.getLooper().isCurrentThread()) { action.run(); } else { // Posts the runnable if this is not called from the main thread mHandler.post(action); } } } } private final class TimeShiftPositionTrackingRunnable implements Runnable { @Override public void run() { long startPositionMs = onTimeShiftGetStartPosition(); if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME || mStartPositionMs != startPositionMs) { mStartPositionMs = startPositionMs; notifyTimeShiftStartPositionChanged(startPositionMs); } long currentPositionMs = onTimeShiftGetCurrentPosition(); if (currentPositionMs < mStartPositionMs) { Log.w(TAG, "Current position (" + currentPositionMs + ") cannot be earlier than" + " start position (" + mStartPositionMs + "). Reset to the start " + "position."); currentPositionMs = mStartPositionMs; } if (mCurrentPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME || mCurrentPositionMs != currentPositionMs) { mCurrentPositionMs = currentPositionMs; notifyTimeShiftCurrentPositionChanged(currentPositionMs); } mHandler.removeCallbacks(mTimeShiftPositionTrackingRunnable); mHandler.postDelayed(mTimeShiftPositionTrackingRunnable, POSITION_UPDATE_INTERVAL_MS); } } } private static final class OverlayViewCleanUpTask extends AsyncTask { @Override protected Void doInBackground(View... views) { View overlayViewParent = views[0]; try { Thread.sleep(DETACH_OVERLAY_VIEW_TIMEOUT_MS); } catch (InterruptedException e) { return null; } if (isCancelled()) { return null; } if (overlayViewParent.isAttachedToWindow()) { Log.e(TAG, "Time out on releasing overlay view. Killing " + overlayViewParent.getContext().getPackageName()); Process.killProcess(Process.myPid()); } return null; } } /** * Base class for derived classes to implement to provide a TV input recording session. */ public abstract static class RecordingSession { final Handler mHandler; private final Object mLock = new Object(); // @GuardedBy("mLock") private ITvInputSessionCallback mSessionCallback; // @GuardedBy("mLock") private final List mPendingActions = new ArrayList<>(); /** * Creates a new RecordingSession. * * @param context The context of the application */ public RecordingSession(Context context) { mHandler = new Handler(context.getMainLooper()); } /** * Informs the application that this recording session has been tuned to the given channel * and is ready to start recording. * *

Upon receiving a call to {@link #onTune(Uri)}, the session is expected to tune to the * passed channel and call this method to indicate that it is now available for immediate * recording. When {@link #onStartRecording(Uri)} is called, recording must start with * minimal delay. * * @param channelUri The URI of a channel. */ public void notifyTuned(Uri channelUri) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyTuned"); if (mSessionCallback != null) { mSessionCallback.onTuned(channelUri); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTuned", e); } } }); } /** * Informs the application that this recording session has stopped recording and created a * new data entry in the {@link TvContract.RecordedPrograms} table that describes the newly * recorded program. * *

The recording session must call this method in response to {@link #onStopRecording()}. * The session may call it even before receiving a call to {@link #onStopRecording()} if a * partially recorded program is available when there is an error. * * @param recordedProgramUri The URI of the newly recorded program. */ public void notifyRecordingStopped(final Uri recordedProgramUri) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyRecordingStopped"); if (mSessionCallback != null) { mSessionCallback.onRecordingStopped(recordedProgramUri); } } catch (RemoteException e) { Log.w(TAG, "error in notifyRecordingStopped", e); } } }); } /** * Informs the application that there is an error and this recording session is no longer * able to start or continue recording. It may be called at any time after the recording * session is created until {@link #onRelease()} is called. * *

The application may release the current session upon receiving the error code through * {@link TvRecordingClient.RecordingCallback#onError(int)}. The session may call * {@link #notifyRecordingStopped(Uri)} if a partially recorded but still playable program * is available, before calling this method. * * @param error The error code. Should be one of the followings. *

*/ public void notifyError(@TvInputManager.RecordingError int error) { if (error < TvInputManager.RECORDING_ERROR_START || error > TvInputManager.RECORDING_ERROR_END) { Log.w(TAG, "notifyError - invalid error code (" + error + ") is changed to RECORDING_ERROR_UNKNOWN."); error = TvInputManager.RECORDING_ERROR_UNKNOWN; } final int validError = error; executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifyError"); if (mSessionCallback != null) { mSessionCallback.onError(validError); } } catch (RemoteException e) { Log.w(TAG, "error in notifyError", e); } } }); } /** * Dispatches an event to the application using this recording session. * * @param eventType The type of the event. * @param eventArgs Optional arguments of the event. * @hide */ @SystemApi public void notifySessionEvent(@NonNull final String eventType, final Bundle eventArgs) { Preconditions.checkNotNull(eventType); executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) Log.d(TAG, "notifySessionEvent(" + eventType + ")"); if (mSessionCallback != null) { mSessionCallback.onSessionEvent(eventType, eventArgs); } } catch (RemoteException e) { Log.w(TAG, "error in sending event (event=" + eventType + ")", e); } } }); } /** * Called when the application requests to tune to a given channel for TV program recording. * *

The application may call this method before starting or after stopping recording, but * not during recording. * *

The session must call {@link #notifyTuned(Uri)} if the tune request was fulfilled, or * {@link #notifyError(int)} otherwise. * * @param channelUri The URI of a channel. */ public abstract void onTune(Uri channelUri); /** * Called when the application requests to tune to a given channel for TV program recording. * Override this method in order to handle domain-specific features that are only known * between certain TV inputs and their clients. * *

The application may call this method before starting or after stopping recording, but * not during recording. The default implementation calls {@link #onTune(Uri)}. * *

The session must call {@link #notifyTuned(Uri)} if the tune request was fulfilled, or * {@link #notifyError(int)} otherwise. * * @param channelUri The URI of a channel. * @param 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. */ public void onTune(Uri channelUri, Bundle params) { onTune(channelUri); } /** * Called when the application requests to start TV program recording. Recording must start * immediately when this method is called. * *

The application may supply the URI for a TV program for filling in program specific * data fields in the {@link android.media.tv.TvContract.RecordedPrograms} table. * A non-null {@code programUri} implies the started recording should be of that specific * program, whereas null {@code programUri} does not impose such a requirement and the * recording can span across multiple TV programs. In either case, the application must call * {@link TvRecordingClient#stopRecording()} to stop the recording. * *

The session must call {@link #notifyError(int)} if the start request cannot be * fulfilled. * * @param programUri The URI for the TV program to record, built by * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. */ public abstract void onStartRecording(@Nullable Uri programUri); /** * Called when the application requests to stop TV program recording. Recording must stop * immediately when this method is called. * *

The session must create a new data entry in the * {@link android.media.tv.TvContract.RecordedPrograms} table that describes the newly * recorded program and call {@link #notifyRecordingStopped(Uri)} with the URI to that * entry. * If the stop request cannot be fulfilled, the session must call {@link #notifyError(int)}. * */ public abstract void onStopRecording(); /** * Called when the application requests to release all the resources held by this recording * session. */ public abstract void onRelease(); /** * Processes a private command sent from the application to the TV input. This can be used * to provide domain-specific features that are only known between certain TV inputs and * their clients. * * @param action Name of the command to be performed. This must be a scoped name, * i.e. prefixed with a package name you own, so that different developers will * not create conflicting commands. * @param data Any data to include with the command. */ public void onAppPrivateCommand(@NonNull String action, Bundle data) { } /** * Calls {@link #onTune(Uri, Bundle)}. * */ void tune(Uri channelUri, Bundle params) { onTune(channelUri, params); } /** * Calls {@link #onRelease()}. * */ void release() { onRelease(); } /** * Calls {@link #onStartRecording(Uri)}. * */ void startRecording(@Nullable Uri programUri) { onStartRecording(programUri); } /** * Calls {@link #onStopRecording()}. * */ void stopRecording() { onStopRecording(); } /** * Calls {@link #onAppPrivateCommand(String, Bundle)}. */ void appPrivateCommand(String action, Bundle data) { onAppPrivateCommand(action, data); } private void initialize(ITvInputSessionCallback callback) { synchronized(mLock) { mSessionCallback = callback; for (Runnable runnable : mPendingActions) { runnable.run(); } mPendingActions.clear(); } } private void executeOrPostRunnableOnMainThread(Runnable action) { synchronized(mLock) { if (mSessionCallback == null) { // The session is not initialized yet. mPendingActions.add(action); } else { if (mHandler.getLooper().isCurrentThread()) { action.run(); } else { // Posts the runnable if this is not called from the main thread mHandler.post(action); } } } } } /** * Base class for a TV input session which represents an external device connected to a * hardware TV input. * *

This class is for an input which provides channels for the external set-top box to the * application. Once a TV input returns an implementation of this class on * {@link #onCreateSession(String)}, the framework will create a separate session for * a hardware TV Input (e.g. HDMI 1) and forward the application's surface to the session so * that the user can see the screen of the hardware TV Input when she tunes to a channel from * this TV input. The implementation of this class is expected to change the channel of the * external set-top box via a proprietary protocol when {@link HardwareSession#onTune} is * requested by the application. * *

Note that this class is not for inputs for internal hardware like built-in tuner and HDMI * 1. * * @see #onCreateSession(String) */ public abstract static class HardwareSession extends Session { /** * Creates a new HardwareSession. * * @param context The context of the application */ public HardwareSession(Context context) { super(context); } private TvInputManager.Session mHardwareSession; private ITvInputSession mProxySession; private ITvInputSessionCallback mProxySessionCallback; private Handler mServiceHandler; /** * Returns the hardware TV input ID the external device is connected to. * *

TV input is expected to provide {@link android.R.attr#setupActivity} so that * the application can launch it before using this TV input. The setup activity may let * the user select the hardware TV input to which the external device is connected. The ID * of the selected one should be stored in the TV input so that it can be returned here. */ public abstract String getHardwareInputId(); private final TvInputManager.SessionCallback mHardwareSessionCallback = new TvInputManager.SessionCallback() { @Override public void onSessionCreated(TvInputManager.Session session) { mHardwareSession = session; SomeArgs args = SomeArgs.obtain(); if (session != null) { args.arg1 = HardwareSession.this; args.arg2 = mProxySession; args.arg3 = mProxySessionCallback; args.arg4 = session.getToken(); session.tune(TvContract.buildChannelUriForPassthroughInput( getHardwareInputId())); } else { args.arg1 = null; args.arg2 = null; args.arg3 = mProxySessionCallback; args.arg4 = null; onRelease(); } mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, args) .sendToTarget(); } @Override public void onVideoAvailable(final TvInputManager.Session session) { if (mHardwareSession == session) { onHardwareVideoAvailable(); } } @Override public void onVideoUnavailable(final TvInputManager.Session session, final int reason) { if (mHardwareSession == session) { onHardwareVideoUnavailable(reason); } } }; /** * This method will not be called in {@link HardwareSession}. Framework will * forward the application's surface to the hardware TV input. */ @Override public final boolean onSetSurface(Surface surface) { Log.e(TAG, "onSetSurface() should not be called in HardwareProxySession."); return false; } /** * Called when the underlying hardware TV input session calls * {@link TvInputService.Session#notifyVideoAvailable()}. */ public void onHardwareVideoAvailable() { } /** * Called when the underlying hardware TV input session calls * {@link TvInputService.Session#notifyVideoUnavailable(int)}. * * @param reason The reason that the hardware TV input stopped the playback: *

*/ public void onHardwareVideoUnavailable(int reason) { } @Override void release() { if (mHardwareSession != null) { mHardwareSession.release(); mHardwareSession = null; } super.release(); } } /** @hide */ public static boolean isNavigationKey(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_PAGE_DOWN: case KeyEvent.KEYCODE_MOVE_HOME: case KeyEvent.KEYCODE_MOVE_END: case KeyEvent.KEYCODE_TAB: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_ENTER: return true; } return false; } @SuppressLint("HandlerLeak") private final class ServiceHandler extends Handler { private static final int DO_CREATE_SESSION = 1; private static final int DO_NOTIFY_SESSION_CREATED = 2; private static final int DO_CREATE_RECORDING_SESSION = 3; private static final int DO_ADD_HARDWARE_INPUT = 4; private static final int DO_REMOVE_HARDWARE_INPUT = 5; private static final int DO_ADD_HDMI_INPUT = 6; private static final int DO_REMOVE_HDMI_INPUT = 7; private void broadcastAddHardwareInput(int deviceId, TvInputInfo inputInfo) { int n = mCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { try { mCallbacks.getBroadcastItem(i).addHardwareInput(deviceId, inputInfo); } catch (RemoteException e) { Log.e(TAG, "error in broadcastAddHardwareInput", e); } } mCallbacks.finishBroadcast(); } private void broadcastAddHdmiInput(int id, TvInputInfo inputInfo) { int n = mCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { try { mCallbacks.getBroadcastItem(i).addHdmiInput(id, inputInfo); } catch (RemoteException e) { Log.e(TAG, "error in broadcastAddHdmiInput", e); } } mCallbacks.finishBroadcast(); } private void broadcastRemoveHardwareInput(String inputId) { int n = mCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { try { mCallbacks.getBroadcastItem(i).removeHardwareInput(inputId); } catch (RemoteException e) { Log.e(TAG, "error in broadcastRemoveHardwareInput", e); } } mCallbacks.finishBroadcast(); } @Override public final void handleMessage(Message msg) { switch (msg.what) { case DO_CREATE_SESSION: { SomeArgs args = (SomeArgs) msg.obj; InputChannel channel = (InputChannel) args.arg1; ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg2; String inputId = (String) args.arg3; args.recycle(); Session sessionImpl = onCreateSession(inputId); if (sessionImpl == null) { try { // Failed to create a session. cb.onSessionCreated(null, null); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } return; } ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, sessionImpl, channel); if (sessionImpl instanceof HardwareSession) { HardwareSession proxySession = ((HardwareSession) sessionImpl); String hardwareInputId = proxySession.getHardwareInputId(); if (TextUtils.isEmpty(hardwareInputId) || !isPassthroughInput(hardwareInputId)) { if (TextUtils.isEmpty(hardwareInputId)) { Log.w(TAG, "Hardware input id is not setup yet."); } else { Log.w(TAG, "Invalid hardware input id : " + hardwareInputId); } sessionImpl.onRelease(); try { cb.onSessionCreated(null, null); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } return; } proxySession.mProxySession = stub; proxySession.mProxySessionCallback = cb; proxySession.mServiceHandler = mServiceHandler; TvInputManager manager = (TvInputManager) getSystemService( Context.TV_INPUT_SERVICE); manager.createSession(hardwareInputId, proxySession.mHardwareSessionCallback, mServiceHandler); } else { SomeArgs someArgs = SomeArgs.obtain(); someArgs.arg1 = sessionImpl; someArgs.arg2 = stub; someArgs.arg3 = cb; someArgs.arg4 = null; mServiceHandler.obtainMessage(ServiceHandler.DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget(); } return; } case DO_NOTIFY_SESSION_CREATED: { SomeArgs args = (SomeArgs) msg.obj; Session sessionImpl = (Session) args.arg1; ITvInputSession stub = (ITvInputSession) args.arg2; ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg3; IBinder hardwareSessionToken = (IBinder) args.arg4; try { cb.onSessionCreated(stub, hardwareSessionToken); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } if (sessionImpl != null) { sessionImpl.initialize(cb); } args.recycle(); return; } case DO_CREATE_RECORDING_SESSION: { SomeArgs args = (SomeArgs) msg.obj; ITvInputSessionCallback cb = (ITvInputSessionCallback) args.arg1; String inputId = (String) args.arg2; args.recycle(); RecordingSession recordingSessionImpl = onCreateRecordingSession(inputId); if (recordingSessionImpl == null) { try { // Failed to create a recording session. cb.onSessionCreated(null, null); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } return; } ITvInputSession stub = new ITvInputSessionWrapper(TvInputService.this, recordingSessionImpl); try { cb.onSessionCreated(stub, null); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } recordingSessionImpl.initialize(cb); return; } case DO_ADD_HARDWARE_INPUT: { TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj; TvInputInfo inputInfo = onHardwareAdded(hardwareInfo); if (inputInfo != null) { broadcastAddHardwareInput(hardwareInfo.getDeviceId(), inputInfo); } return; } case DO_REMOVE_HARDWARE_INPUT: { TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj; String inputId = onHardwareRemoved(hardwareInfo); if (inputId != null) { broadcastRemoveHardwareInput(inputId); } return; } case DO_ADD_HDMI_INPUT: { HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj; TvInputInfo inputInfo = onHdmiDeviceAdded(deviceInfo); if (inputInfo != null) { broadcastAddHdmiInput(deviceInfo.getId(), inputInfo); } return; } case DO_REMOVE_HDMI_INPUT: { HdmiDeviceInfo deviceInfo = (HdmiDeviceInfo) msg.obj; String inputId = onHdmiDeviceRemoved(deviceInfo); if (inputId != null) { broadcastRemoveHardwareInput(inputId); } return; } default: { Log.w(TAG, "Unhandled message code: " + msg.what); return; } } } } }