/* * Copyright 2021 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.interactive; import android.annotation.CallSuper; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Px; import android.annotation.SdkConstant; import android.annotation.StringDef; import android.annotation.SuppressLint; 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.media.PlaybackParams; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; import android.media.tv.AdResponse; import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.flags.Flags; import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback; import android.net.Uri; import android.net.http.SslCertificate; 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.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.widget.FrameLayout; import com.android.internal.os.SomeArgs; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * A TV interactive application service is a service that provides runtime environment and runs TV * interactive applications. */ public abstract class TvInteractiveAppService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvInteractiveAppService"; private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000; /** * This is the interface name that a service implementing a TV Interactive App service should * say that it supports -- 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_INTERACTIVE_APP} permission so that other * applications cannot abuse it. */ @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = "android.media.tv.interactive.TvInteractiveAppService"; /** * Name under which a TvInteractiveAppService component publishes information about itself. This * meta-data must reference an XML resource containing an * <{@link android.R.styleable#TvInteractiveAppService tv-interactive-app}> * tag. */ public static final String SERVICE_META_DATA = "android.media.tv.interactive.app"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef(prefix = "PLAYBACK_COMMAND_TYPE_", value = { PLAYBACK_COMMAND_TYPE_TUNE, PLAYBACK_COMMAND_TYPE_TUNE_NEXT, PLAYBACK_COMMAND_TYPE_TUNE_PREV, PLAYBACK_COMMAND_TYPE_STOP, PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME, PLAYBACK_COMMAND_TYPE_SELECT_TRACK, PLAYBACK_COMMAND_TYPE_FREEZE }) public @interface PlaybackCommandType {} /** * Playback command type: tune to the given channel. * @see #COMMAND_PARAMETER_KEY_CHANNEL_URI */ public static final String PLAYBACK_COMMAND_TYPE_TUNE = "tune"; /** * Playback command type: tune to the next channel. */ public static final String PLAYBACK_COMMAND_TYPE_TUNE_NEXT = "tune_next"; /** * Playback command type: tune to the previous channel. */ public static final String PLAYBACK_COMMAND_TYPE_TUNE_PREV = "tune_previous"; /** * Playback command type: stop the playback. */ public static final String PLAYBACK_COMMAND_TYPE_STOP = "stop"; /** * Playback command type: set the volume. */ public static final String PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME = "set_stream_volume"; /** * Playback command type: select the given track. */ public static final String PLAYBACK_COMMAND_TYPE_SELECT_TRACK = "select_track"; /** * Playback command type: freeze the video playback on the current frame. * @hide */ public static final String PLAYBACK_COMMAND_TYPE_FREEZE = "freeze"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "COMMAND_PARAMETER_VALUE_STOP_MODE_", value = { COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK, COMMAND_PARAMETER_VALUE_STOP_MODE_FREEZE }) public @interface PlaybackCommandStopMode {} /** * Playback command stop mode: show a blank screen. * @see #PLAYBACK_COMMAND_TYPE_STOP */ public static final int COMMAND_PARAMETER_VALUE_STOP_MODE_BLANK = 1; /** * Playback command stop mode: freeze the video. * @see #PLAYBACK_COMMAND_TYPE_STOP */ public static final int COMMAND_PARAMETER_VALUE_STOP_MODE_FREEZE = 2; /** * Playback command parameter: stop mode. *

Type: int * * @see #PLAYBACK_COMMAND_TYPE_STOP */ public static final String COMMAND_PARAMETER_KEY_STOP_MODE = "command_stop_mode"; /** * Playback command parameter: channel URI. *

Type: android.net.Uri * * @see #PLAYBACK_COMMAND_TYPE_TUNE */ public static final String COMMAND_PARAMETER_KEY_CHANNEL_URI = "command_channel_uri"; /** * Playback command parameter: TV input ID. *

Type: String * * @see TvInputInfo#getId() */ public static final String COMMAND_PARAMETER_KEY_INPUT_ID = "command_input_id"; /** * Playback command parameter: stream volume. *

Type: float * * @see #PLAYBACK_COMMAND_TYPE_SET_STREAM_VOLUME */ public static final String COMMAND_PARAMETER_KEY_VOLUME = "command_volume"; /** * Playback command parameter: track type. *

Type: int * * @see #PLAYBACK_COMMAND_TYPE_SELECT_TRACK * @see TvTrackInfo#getType() */ public static final String COMMAND_PARAMETER_KEY_TRACK_TYPE = "command_track_type"; /** * Playback command parameter: track ID. *

Type: String * * @see #PLAYBACK_COMMAND_TYPE_SELECT_TRACK * @see TvTrackInfo#getId() */ public static final String COMMAND_PARAMETER_KEY_TRACK_ID = "command_track_id"; /** * Command to quiet channel change. No channel banner or channel info is shown. *

Refer to HbbTV Spec 2.0.4 chapter A.2.4.3. */ public static final String COMMAND_PARAMETER_KEY_CHANGE_CHANNEL_QUIETLY = "command_change_channel_quietly"; /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef(prefix = "TIME_SHIFT_COMMAND_TYPE_", value = { TIME_SHIFT_COMMAND_TYPE_PLAY, TIME_SHIFT_COMMAND_TYPE_PAUSE, TIME_SHIFT_COMMAND_TYPE_RESUME, TIME_SHIFT_COMMAND_TYPE_SEEK_TO, TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS, TIME_SHIFT_COMMAND_TYPE_SET_MODE, }) public @interface TimeShiftCommandType {} /** * Time shift command type: play. * * @see TvView#timeShiftPlay(String, Uri) */ public static final String TIME_SHIFT_COMMAND_TYPE_PLAY = "play"; /** * Time shift command type: pause. * * @see TvView#timeShiftPause() */ public static final String TIME_SHIFT_COMMAND_TYPE_PAUSE = "pause"; /** * Time shift command type: resume. * * @see TvView#timeShiftResume() */ public static final String TIME_SHIFT_COMMAND_TYPE_RESUME = "resume"; /** * Time shift command type: seek to. * * @see TvView#timeShiftSeekTo(long) */ public static final String TIME_SHIFT_COMMAND_TYPE_SEEK_TO = "seek_to"; /** * Time shift command type: set playback params. * * @see TvView#timeShiftSetPlaybackParams(PlaybackParams) */ public static final String TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS = "set_playback_params"; /** * Time shift command type: set time shift mode. */ public static final String TIME_SHIFT_COMMAND_TYPE_SET_MODE = "set_mode"; /** * Time shift command parameter: program URI. *

Type: android.net.Uri * * @see #TIME_SHIFT_COMMAND_TYPE_PLAY */ public static final String COMMAND_PARAMETER_KEY_PROGRAM_URI = "command_program_uri"; /** * Time shift command parameter: time position for time shifting, in milliseconds. *

Type: long * * @see #TIME_SHIFT_COMMAND_TYPE_SEEK_TO */ public static final String COMMAND_PARAMETER_KEY_TIME_POSITION = "command_time_position"; /** * Time shift command parameter: playback params. *

Type: android.media.PlaybackParams * * @see #TIME_SHIFT_COMMAND_TYPE_SET_PLAYBACK_PARAMS */ public static final String COMMAND_PARAMETER_KEY_PLAYBACK_PARAMS = "command_playback_params"; /** * Time shift command parameter: playback params. *

Type: Integer. One of {@link TvInputManager#TIME_SHIFT_MODE_OFF}, * {@link TvInputManager#TIME_SHIFT_MODE_LOCAL}, * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK}, * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}. * * @see #TIME_SHIFT_COMMAND_TYPE_SET_MODE */ public static final String COMMAND_PARAMETER_KEY_TIME_SHIFT_MODE = "command_time_shift_mode"; private final Handler mServiceHandler = new ServiceHandler(); private final RemoteCallbackList mCallbacks = new RemoteCallbackList<>(); @Override @Nullable public final IBinder onBind(@NonNull Intent intent) { ITvInteractiveAppService.Stub tvIAppServiceBinder = new ITvInteractiveAppService.Stub() { @Override public void registerCallback(ITvInteractiveAppServiceCallback cb) { if (cb != null) { mCallbacks.register(cb); } } @Override public void unregisterCallback(ITvInteractiveAppServiceCallback cb) { if (cb != null) { mCallbacks.unregister(cb); } } @Override public void createSession(InputChannel channel, ITvInteractiveAppSessionCallback cb, String iAppServiceId, int type) { if (cb == null) { return; } SomeArgs args = SomeArgs.obtain(); args.arg1 = channel; args.arg2 = cb; args.arg3 = iAppServiceId; args.arg4 = type; mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args) .sendToTarget(); } @Override public void registerAppLinkInfo(AppLinkInfo appLinkInfo) { onRegisterAppLinkInfo(appLinkInfo); } @Override public void unregisterAppLinkInfo(AppLinkInfo appLinkInfo) { onUnregisterAppLinkInfo(appLinkInfo); } @Override public void sendAppLinkCommand(Bundle command) { onAppLinkCommand(command); } }; return tvIAppServiceBinder; } /** * Called when a request to register an Android application link info record is received. */ public void onRegisterAppLinkInfo(@NonNull AppLinkInfo appLinkInfo) { } /** * Called when a request to unregister an Android application link info record is received. */ public void onUnregisterAppLinkInfo(@NonNull AppLinkInfo appLinkInfo) { } /** * Called when app link command is received. * * @see android.media.tv.interactive.TvInteractiveAppManager#sendAppLinkCommand(String, Bundle) */ public void onAppLinkCommand(@NonNull Bundle command) { } /** * Returns a concrete implementation of {@link Session}. * *

May return {@code null} if this TV Interactive App service fails to create a session for * some reason. * * @param iAppServiceId The ID of the TV Interactive App associated with the session. * @param type The type of the TV Interactive App associated with the session. */ @Nullable public abstract Session onCreateSession( @NonNull String iAppServiceId, @TvInteractiveAppServiceInfo.InteractiveAppType int type); /** * Notifies the system when the state of the interactive app RTE has been changed. * * @param type the interactive app type * @param state the current state of the service of the given type * @param error the error code for error state. {@link TvInteractiveAppManager#ERROR_NONE} is * used when the state is not * {@link TvInteractiveAppManager#SERVICE_STATE_ERROR}. */ public final void notifyStateChanged( @TvInteractiveAppServiceInfo.InteractiveAppType int type, @TvInteractiveAppManager.ServiceState int state, @TvInteractiveAppManager.ErrorCode int error) { SomeArgs args = SomeArgs.obtain(); args.arg1 = type; args.arg2 = state; args.arg3 = error; mServiceHandler .obtainMessage(ServiceHandler.DO_NOTIFY_RTE_STATE_CHANGED, args).sendToTarget(); } /** * Base class for derived classes to implement to provide a TV interactive app session. * *

A session is associated with a {@link TvInteractiveAppView} instance and handles * corresponding communications. It also handles the communications with * {@link android.media.tv.TvInputService.Session} if connected. * * @see TvInteractiveAppView#setTvView(TvView) */ public abstract static class Session implements KeyEvent.Callback { private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); private final Object mLock = new Object(); // @GuardedBy("mLock") private ITvInteractiveAppSessionCallback mSessionCallback; // @GuardedBy("mLock") private final List mPendingActions = new ArrayList<>(); private final Context mContext; final Handler mHandler; private final WindowManager mWindowManager; private WindowManager.LayoutParams mWindowParams; private Surface mSurface; private FrameLayout mMediaViewContainer; private View mMediaView; private MediaViewCleanUpTask mMediaViewCleanUpTask; private boolean mMediaViewEnabled; private IBinder mWindowToken; private Rect mMediaFrame; /** * Creates a new Session. * * @param context The context of the application */ public Session(@NonNull Context context) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mHandler = new Handler(context.getMainLooper()); } /** * Enables or disables the media view. * *

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

The TV Interactive App service can disable its media view when needed. * * @param enable {@code true} if you want to enable the media view. {@code false} * otherwise. */ @CallSuper public void setMediaViewEnabled(final boolean enable) { mHandler.post(new Runnable() { @Override public void run() { if (enable == mMediaViewEnabled) { return; } mMediaViewEnabled = enable; if (enable) { if (mWindowToken != null) { createMediaView(mWindowToken, mMediaFrame); } } else { removeMediaView(false); } } }); } /** * Returns {@code true} if media view is enabled, {@code false} otherwise. * * @see #setMediaViewEnabled(boolean) */ public boolean isMediaViewEnabled() { return mMediaViewEnabled; } /** * Starts TvInteractiveAppService session. */ public void onStartInteractiveApp() { } /** * Stops TvInteractiveAppService session. */ public void onStopInteractiveApp() { } /** * Resets TvInteractiveAppService session. */ public void onResetInteractiveApp() { } /** * Creates broadcast-independent(BI) interactive application. * *

The implementation should call {@link #notifyBiInteractiveAppCreated(Uri, String)}, * no matter if it's created successfully or not. * * @see #notifyBiInteractiveAppCreated(Uri, String) * @see #onDestroyBiInteractiveAppRequest(String) */ public void onCreateBiInteractiveAppRequest( @NonNull Uri biIAppUri, @Nullable Bundle params) { } /** * Destroys broadcast-independent(BI) interactive application. * * @param biIAppId the BI interactive app ID from * {@link #onCreateBiInteractiveAppRequest(Uri, Bundle)} * * @see #onCreateBiInteractiveAppRequest(Uri, Bundle) */ public void onDestroyBiInteractiveAppRequest(@NonNull String biIAppId) { } /** * To toggle Digital Teletext Application if there is one in AIT app list. * @param enable {@code true} to enable teletext app; {@code false} otherwise. */ public void onSetTeletextAppEnabled(boolean enable) { } /** * Receives current video bounds. * * @param bounds the rectangle area for rendering the current video. */ public void onCurrentVideoBounds(@NonNull Rect bounds) { } /** * Receives current channel URI. */ public void onCurrentChannelUri(@Nullable Uri channelUri) { } /** * Receives logical channel number (LCN) of current channel. */ public void onCurrentChannelLcn(int lcn) { } /** * Receives current stream volume. * * @param volume a volume value between {@code 0.0f} and {@code 1.0f}, inclusive. */ public void onStreamVolume(float volume) { } /** * Receives track list. */ public void onTrackInfoList(@NonNull List tracks) { } /** * Receives current TV input ID. */ public void onCurrentTvInputId(@Nullable String inputId) { } /** * Receives current time shift mode. * * @param mode The current time shift mode. The value is one of the following: * {@link TvInputManager#TIME_SHIFT_MODE_OFF}, {@link TvInputManager#TIME_SHIFT_MODE_LOCAL}, * {@link TvInputManager#TIME_SHIFT_MODE_NETWORK}, * {@link TvInputManager#TIME_SHIFT_MODE_AUTO}. */ public void onTimeShiftMode(@android.media.tv.TvInputManager.TimeShiftMode int mode) { } /** * Receives available playback speeds. * * @param speeds An ordered array of playback speeds, expressed as values relative to the * normal playback speed (1.0), at which the current content can be played as * a time-shifted broadcast. This is an empty array if the supported playback * speeds are unknown or the video/broadcast is not in time shift mode. If * currently in time shift mode, this array will normally include at least * the values 1.0 (normal speed) and 0.0 (paused). */ public void onAvailableSpeeds(@NonNull float[] speeds) { } /** * Receives the requested {@link android.media.tv.TvRecordingInfo}. * * @see #requestTvRecordingInfo(String) * @param recordingInfo The requested recording info. {@code null} if no recording found. */ public void onTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { } /** * Receives requested recording info list. * * @see #requestTvRecordingInfoList(int) * @param recordingInfoList The list of recording info requested. Returns an empty list if * no matching recording info found. */ public void onTvRecordingInfoList(@NonNull List recordingInfoList) {} /** * This is called when a recording has been started. * *

When a scheduled recording is started, this is also called, and the request ID in this * case is {@code null}. * * @param recordingId The ID of the recording started. The TV app should provide and * maintain this ID to identify the recording in the future. * @param requestId The ID of the request when * {@link #requestStartRecording(String, Uri)} is called. * {@code null} if the recording is not triggered by a * {@link #requestStartRecording(String, Uri)} request. * This ID should be created by the {@link TvInteractiveAppService} and * can be any string. * @see #onRecordingStopped(String) */ public void onRecordingStarted(@NonNull String recordingId, @Nullable String requestId) { } /** * This is called when the recording has been stopped. * * @param recordingId The ID of the recording stopped. This ID is created and maintained by * the TV app when the recording was started. * @see #onRecordingStarted(String, String) */ public void onRecordingStopped(@NonNull String recordingId) { } /** * This is called when an error occurred while establishing a connection to the recording * session for the corresponding TV input. * * @param recordingId The ID of the related recording which is sent via * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @param inputId The ID of the TV input bound to the current TvRecordingClient. * @see android.media.tv.TvRecordingClient.RecordingCallback#onConnectionFailed(String) */ public void onRecordingConnectionFailed( @NonNull String recordingId, @NonNull String inputId) { } /** * This is called when the connection to the current recording session is lost. * * @param recordingId The ID of the related recording which is sent via * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @param inputId The ID of the TV input bound to the current TvRecordingClient. * @see android.media.tv.TvRecordingClient.RecordingCallback#onDisconnected(String) */ public void onRecordingDisconnected(@NonNull String recordingId, @NonNull String inputId) { } /** * This is called when the recording session has been tuned to the given channel and is * ready to start recording. * * @param recordingId The ID of the related recording which is sent via * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @param channelUri The URI of the tuned channel. * @see android.media.tv.TvRecordingClient.RecordingCallback#onTuned(Uri) */ public void onRecordingTuned(@NonNull String recordingId, @NonNull Uri channelUri) { } /** * This is called when an issue has occurred. It may be called at any time after the current * recording session is created until it is released. * * @param recordingId The ID of the related recording which is sent via * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @param err The error code. Should be one of the following. *

* @see android.media.tv.TvRecordingClient.RecordingCallback#onError(int) */ public void onRecordingError( @NonNull String recordingId, @TvInputManager.RecordingError int err) { } /** * This is called when the recording has been scheduled. * * @param recordingId The ID assigned to this recording by the app. It can be used to send * recording related requests such as * {@link #requestStopRecording(String)}. * @param requestId The ID of the request when * {@link #requestScheduleRecording} is called. * {@code null} if the recording is not triggered by a request. * This ID should be created by the {@link TvInteractiveAppService} and * can be any string. */ public void onRecordingScheduled(@NonNull String recordingId, @Nullable String requestId) { } /** * Receives signing result. * @param signingId the ID to identify the request. It's the same as the corresponding ID in * {@link Session#requestSigning(String, String, String, byte[])} * @param result the signed result. * * @see #requestSigning(String, String, String, byte[]) */ public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) { } /** * Receives the requested Certificate * * @param host the host name of the SSL authentication server. * @param port the port of the SSL authentication server. E.g., 443 * @param cert the SSL certificate received. */ @FlaggedApi(Flags.FLAG_TIAF_V_APIS) public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { } /** * Called when the application sends information of an error. * * @param errMsg the message of the error. * @param params additional parameters of the error. For example, the signingId of {@link * TvInteractiveAppCallback#onRequestSigning(String, String, String, String, byte[])} * can be included to identify the related signing request, and the method name * "onRequestSigning" can also be added to the params. * * @see TvInteractiveAppView#ERROR_KEY_METHOD_NAME */ public void onError(@NonNull String errMsg, @NonNull Bundle params) { } /** * Called when the time shift {@link android.media.PlaybackParams} is set or changed. * * @param params The new {@link PlaybackParams} that was set or changed. * @see TvView#timeShiftSetPlaybackParams(PlaybackParams) */ public void onTimeShiftPlaybackParams(@NonNull PlaybackParams params) { } /** * Called when time shift status is changed. * * @see TvView.TvInputCallback#onTimeShiftStatusChanged(String, int) * @see android.media.tv.TvInputService.Session#notifyTimeShiftStatusChanged(int) * @param inputId The ID of the input for which the time shift status has changed. * @param status The status of which the input has changed to. Should be one of the * following. * */ public void onTimeShiftStatusChanged( @NonNull String inputId, @TvInputManager.TimeShiftStatus int status) {} /** * Called when time shift start position is changed. * * @see TvView.TimeShiftPositionCallback#onTimeShiftStartPositionChanged(String, long) * @param inputId The ID of the input for which the time shift start position has changed. * @param timeMs The start position for time shifting, in milliseconds since the epoch. */ public void onTimeShiftStartPositionChanged(@NonNull String inputId, long timeMs) { } /** * Called when time shift current position is changed. * * @see TvView.TimeShiftPositionCallback#onTimeShiftCurrentPositionChanged(String, long) * @param inputId The ID of the input for which the time shift current position has changed. * @param timeMs The current position for time shifting, in milliseconds since the epoch. */ public void onTimeShiftCurrentPositionChanged(@NonNull String inputId, long timeMs) { } /** * Called when the application sets the surface. * *

The TV Interactive App service should render interactive app UI onto the given * surface. When called with {@code null}, the Interactive App service should immediately * free any references to the currently set surface and stop using it. * * @param surface The surface to be used for interactive app UI 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 {@link PixelFormat} of the surface. * @param width The new width of the surface. * @param height The new height of the surface. */ public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) { } /** * Called when the size of the media view is changed by the application. * *

This is always called at least once when the session is created regardless of whether * the media view is enabled or not. The media view container size is the same as the * containing {@link TvInteractiveAppView}. 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 media view, in pixels. * @param height The height of the media view, in pixels. */ public void onMediaViewSizeChanged(@Px int width, @Px int height) { } /** * Called when the application requests to create an media view. Each session * implementation can override this method and return its own view. * * @return a view attached to the media window */ @Nullable public View onCreateMediaView() { return null; } /** * Releases TvInteractiveAppService session. */ public abstract void onRelease(); /** * Called when the corresponding TV input tuned to a channel. * * @param channelUri The tuned channel URI. */ public void onTuned(@NonNull Uri channelUri) { } /** * Called when the corresponding TV input selected to a track. * * If the track is deselected and no track is currently selected, * trackId is an empty string. */ public void onTrackSelected(@TvTrackInfo.Type int type, @NonNull String trackId) { } /** * Called when the tracks are changed. */ public void onTracksChanged(@NonNull List tracks) { } /** * Called when video is available. */ public void onVideoAvailable() { } /** * Called when video is unavailable. */ public void onVideoUnavailable(@TvInputManager.VideoUnavailableReason int reason) { } /** * Called when video becomes frozen or unfrozen. Audio playback will continue while video * will be frozen to the last frame if {@code true}. * * @param isFrozen Whether or not the video is frozen. */ @FlaggedApi(Flags.FLAG_TIAF_V_APIS) public void onVideoFreezeUpdated(boolean isFrozen) {} /** * Called when content is allowed. */ public void onContentAllowed() { } /** * Called when content is blocked. */ public void onContentBlocked(@NonNull TvContentRating rating) { } /** * Called when signal strength is changed. */ public void onSignalStrength(@TvInputManager.SignalStrength int strength) { } /** * Called when a broadcast info response is received. */ public void onBroadcastInfoResponse(@NonNull BroadcastInfoResponse response) { } /** * Called when an advertisement response is received. */ public void onAdResponse(@NonNull AdResponse response) { } /** * Called when an advertisement buffer is consumed. * * @param buffer The {@link AdBuffer} that was consumed. */ public void onAdBufferConsumed(@NonNull AdBuffer buffer) { } /** * Called when a TV message is received * * @param type The type of message received, such as * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} * @param data The raw data of the message. The bundle keys are: * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on * how to parse this data. */ public void onTvMessage(@TvInputManager.TvMessageType int type, @NonNull Bundle data) { } /** * Called when the TV App sends the selected track info as a response to * {@link #requestSelectedTrackInfo()}. * *

When a selected track changes as a result of a new selection, * {@link #onTrackSelected(int, String)} should be used instead to communicate the specific * track selection. * * @param tracks A list of {@link TvTrackInfo} that are currently selected */ @FlaggedApi(Flags.FLAG_TIAF_V_APIS) public void onSelectedTrackInfo(@NonNull List tracks) { } @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { return false; } @Override public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { return false; } @Override public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) { return false; } @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { return false; } /** * Implement this method to handle touch screen motion events on the current 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(@NonNull MotionEvent event) { return false; } /** * Implement this method to handle trackball events on the current 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(@NonNull MotionEvent event) { return false; } /** * Implement this method to handle generic motion events on the current 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(@NonNull MotionEvent event) { return false; } /** * 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. */ @CallSuper 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); } } }); } /** * Requests broadcast related information from the related TV input. * @param request the request for broadcast info */ @CallSuper public void requestBroadcastInfo(@NonNull final BroadcastInfoRequest request) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestBroadcastInfo (requestId=" + request.getRequestId() + ")"); } if (mSessionCallback != null) { mSessionCallback.onBroadcastInfoRequest(request); } } catch (RemoteException e) { Log.w(TAG, "error in requestBroadcastInfo", e); } } }); } /** * Remove broadcast information request from the related TV input. * @param requestId the ID of the request */ @CallSuper public void removeBroadcastInfo(final int requestId) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "removeBroadcastInfo (requestId=" + requestId + ")"); } if (mSessionCallback != null) { mSessionCallback.onRemoveBroadcastInfo(requestId); } } catch (RemoteException e) { Log.w(TAG, "error in removeBroadcastInfo", e); } } }); } /** * Sends a specific playback command to be processed by the related TV input. * * @param cmdType type of the specific command * @param parameters parameters of the specific command */ @CallSuper public void sendPlaybackCommandRequest( @PlaybackCommandType @NonNull String cmdType, @Nullable Bundle parameters) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCommand (cmdType=" + cmdType + ", parameters=" + parameters.toString() + ")"); } if (mSessionCallback != null) { mSessionCallback.onCommandRequest(cmdType, parameters); } } catch (RemoteException e) { Log.w(TAG, "error in requestCommand", e); } } }); } /** * Sends a specific time shift command to be processed by the related TV input. * * @param cmdType type of the specific command * @param parameters parameters of the specific command */ @CallSuper public void sendTimeShiftCommandRequest( @TimeShiftCommandType @NonNull String cmdType, @Nullable Bundle parameters) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestTimeShiftCommand (cmdType=" + cmdType + ", parameters=" + parameters.toString() + ")"); } if (mSessionCallback != null) { mSessionCallback.onTimeShiftCommandRequest(cmdType, parameters); } } catch (RemoteException e) { Log.w(TAG, "error in requestTimeShiftCommand", e); } } }); } /** * Sets broadcast video bounds. */ @CallSuper public void setVideoBounds(@NonNull Rect rect) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "setVideoBounds (rect=" + rect + ")"); } if (mSessionCallback != null) { mSessionCallback.onSetVideoBounds(rect); } } catch (RemoteException e) { Log.w(TAG, "error in setVideoBounds", e); } } }); } /** * Requests the bounds of the current video. */ @CallSuper public void requestCurrentVideoBounds() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCurrentVideoBounds"); } if (mSessionCallback != null) { mSessionCallback.onRequestCurrentVideoBounds(); } } catch (RemoteException e) { Log.w(TAG, "error in requestCurrentVideoBounds", e); } } }); } /** * Requests the URI of the current channel. */ @CallSuper public void requestCurrentChannelUri() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCurrentChannelUri"); } if (mSessionCallback != null) { mSessionCallback.onRequestCurrentChannelUri(); } } catch (RemoteException e) { Log.w(TAG, "error in requestCurrentChannelUri", e); } } }); } /** * Requests the logic channel number (LCN) of the current channel. */ @CallSuper public void requestCurrentChannelLcn() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCurrentChannelLcn"); } if (mSessionCallback != null) { mSessionCallback.onRequestCurrentChannelLcn(); } } catch (RemoteException e) { Log.w(TAG, "error in requestCurrentChannelLcn", e); } } }); } /** * Requests stream volume. */ @CallSuper public void requestStreamVolume() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestStreamVolume"); } if (mSessionCallback != null) { mSessionCallback.onRequestStreamVolume(); } } catch (RemoteException e) { Log.w(TAG, "error in requestStreamVolume", e); } } }); } /** * Requests the list of {@link TvTrackInfo}. */ @CallSuper public void requestTrackInfoList() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestTrackInfoList"); } if (mSessionCallback != null) { mSessionCallback.onRequestTrackInfoList(); } } catch (RemoteException e) { Log.w(TAG, "error in requestTrackInfoList", e); } } }); } /** * Requests current TV input ID. * * @see android.media.tv.TvInputInfo */ @CallSuper public void requestCurrentTvInputId() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCurrentTvInputId"); } if (mSessionCallback != null) { mSessionCallback.onRequestCurrentTvInputId(); } } catch (RemoteException e) { Log.w(TAG, "error in requestCurrentTvInputId", e); } } }); } /** * Requests time shift mode. */ @CallSuper public void requestTimeShiftMode() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestTimeShiftMode"); } if (mSessionCallback != null) { mSessionCallback.onRequestTimeShiftMode(); } } catch (RemoteException e) { Log.w(TAG, "error in requestTimeShiftMode", e); } } }); } /** * Requests available speeds for time shift. */ @CallSuper public void requestAvailableSpeeds() { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestAvailableSpeeds"); } if (mSessionCallback != null) { mSessionCallback.onRequestAvailableSpeeds(); } } catch (RemoteException e) { Log.w(TAG, "error in requestAvailableSpeeds", e); } } }); } /** * Requests a list of the currently selected {@link TvTrackInfo} from the TV App. * *

Normally, track info cannot be synchronized until the channel has * been changed. This is used when the session of the {@link TvInteractiveAppService} * is newly created and the normal synchronization has not happened yet. * *

The track info will be returned in {@link #onSelectedTrackInfo(List)} */ @FlaggedApi(Flags.FLAG_TIAF_V_APIS) @CallSuper public void requestSelectedTrackInfo() { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestSelectedTrackInfo"); } if (mSessionCallback != null) { mSessionCallback.onRequestSelectedTrackInfo(); } } catch (RemoteException e) { Log.w(TAG, "error in requestSelectedTrackInfo", e); } }); } /** * Requests starting of recording * *

This is used to request the active {@link android.media.tv.TvRecordingClient} to * call {@link android.media.tv.TvRecordingClient#startRecording(Uri)} with the provided * {@code programUri}. * 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. * * @param requestId The ID of this request which is used to match the corresponding * response. The request ID in * {@link #onRecordingStarted(String, String)} for this request is the * same as the ID sent here. This should be defined by the * {@link TvInteractiveAppService} and can be any string. * Should this API be called with the same requestId twice, both * requests should be handled regardless by the TV application. * @param programUri The URI for the TV program to record, built by * {@link TvContract#buildProgramUri(long)}. Can be {@code null}. * @see android.media.tv.TvRecordingClient#startRecording(Uri) */ @CallSuper public void requestStartRecording(@NonNull String requestId, @Nullable Uri programUri) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestStartRecording"); } if (mSessionCallback != null) { mSessionCallback.onRequestStartRecording(requestId, programUri); } } catch (RemoteException e) { Log.w(TAG, "error in requestStartRecording", e); } }); } /** * Requests the recording associated with the recordingId to stop. * *

This is used to request the associated {@link android.media.tv.TvRecordingClient} to * call {@link android.media.tv.TvRecordingClient#stopRecording()}. * * @param recordingId The ID of the recording to stop. This is provided by the TV app in * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @see android.media.tv.TvRecordingClient#stopRecording() */ @CallSuper public void requestStopRecording(@NonNull String recordingId) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestStopRecording"); } if (mSessionCallback != null) { mSessionCallback.onRequestStopRecording(recordingId); } } catch (RemoteException e) { Log.w(TAG, "error in requestStopRecording", e); } }); } /** * Requests scheduling of a recording. * * @param requestId The ID of this request which is used to match the corresponding * response. The request ID in * {@link #onRecordingScheduled(String, String)} for this request is the * same as the ID sent here. This should be defined by the * {@link TvInteractiveAppService} and can be any string. * Should this API be called with the same requestId twice, both requests * should be handled regardless by the TV application. * @param inputId The ID of the TV input for the given channel. * @param channelUri The URI of a channel to be recorded. * @param programUri The URI of the TV program to be recorded. * @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. * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle) * @see android.media.tv.TvRecordingClient#startRecording(Uri) */ @CallSuper public void requestScheduleRecording(@NonNull String requestId, @NonNull String inputId, @NonNull Uri channelUri, @NonNull Uri programUri, @NonNull Bundle params) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestScheduleRecording"); } if (mSessionCallback != null) { mSessionCallback.onRequestScheduleRecording( requestId, inputId, channelUri, programUri, params); } } catch (RemoteException e) { Log.w(TAG, "error in requestScheduleRecording", e); } }); } /** * Requests scheduling of a recording. * * @param requestId The ID of this request which is used to match the corresponding * response. The request ID in * {@link #onRecordingScheduled(String, String)} for this request is the * same as the ID sent here. This should be defined by the * {@link TvInteractiveAppService} and can be any string. Should this API * be called with the same requestId twice, both requests should be handled * regardless by the TV application. * @param inputId The ID of the TV input for the given channel. * @param channelUri The URI of a channel to be recorded. * @param startTime The start time of the recording in milliseconds since epoch. * @param duration The duration of the recording in milliseconds. * @param repeatDays The repeated days. 0 if not repeated. * @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. * @see android.media.tv.TvRecordingClient#tune(String, Uri, Bundle) * @see android.media.tv.TvRecordingClient#startRecording(Uri) */ @CallSuper public void requestScheduleRecording(@NonNull String requestId, @NonNull String inputId, @NonNull Uri channelUri, long startTime, long duration, int repeatDays, @NonNull Bundle params) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestScheduleRecording"); } if (mSessionCallback != null) { mSessionCallback.onRequestScheduleRecording2(requestId, inputId, channelUri, startTime, duration, repeatDays, params); } } catch (RemoteException e) { Log.w(TAG, "error in requestScheduleRecording", e); } }); } /** * Sets the recording info for the specified recording * * @param recordingId The ID of the recording to set the info for. This is provided by the * TV app in {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} * @param recordingInfo The {@link TvRecordingInfo} to set to the recording. */ @CallSuper public void setTvRecordingInfo( @NonNull String recordingId, @NonNull TvRecordingInfo recordingInfo) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "setTvRecordingInfo"); } if (mSessionCallback != null) { mSessionCallback.onSetTvRecordingInfo(recordingId, recordingInfo); } } catch (RemoteException e) { Log.w(TAG, "error in setTvRecordingInfo", e); } }); } /** * Gets the recording info for the specified recording * @param recordingId The ID of the recording to set the info for. This is provided by the * TV app in * {@link TvInteractiveAppView#notifyRecordingStarted(String, String)} */ @CallSuper public void requestTvRecordingInfo(@NonNull String recordingId) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestTvRecordingInfo"); } if (mSessionCallback != null) { mSessionCallback.onRequestTvRecordingInfo(recordingId); } } catch (RemoteException e) { Log.w(TAG, "error in requestTvRecordingInfo", e); } }); } /** * Gets a list of {@link TvRecordingInfo} for the specified recording type. * * @param type The type of recording to retrieve. */ @CallSuper public void requestTvRecordingInfoList(@TvRecordingInfo.TvRecordingListType int type) { executeOrPostRunnableOnMainThread(() -> { try { if (DEBUG) { Log.d(TAG, "requestTvRecordingInfoList"); } if (mSessionCallback != null) { mSessionCallback.onRequestTvRecordingInfoList(type); } } catch (RemoteException e) { Log.w(TAG, "error in requestTvRecordingInfoList", e); } }); } /** * Requests signing of the given data. * *

This is used when the corresponding server of the broadcast-independent interactive * app requires signing during handshaking, and the interactive app service doesn't have * the built-in private key. The private key is provided by the content providers and * pre-built in the related app, such as TV app. * * @param signingId the ID to identify the request. When a result is received, this ID can * be used to correlate the result with the request. * @param algorithm the standard name of the signature algorithm requested, such as * MD5withRSA, SHA256withDSA, etc. The name is from standards like * FIPS PUB 186-4 and PKCS #1. * @param alias the alias of the corresponding {@link java.security.KeyStore}. * @param data the original bytes to be signed. * * @see #onSigningResult(String, byte[]) * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle) * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS */ @CallSuper public void requestSigning(@NonNull String signingId, @NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestSigning"); } if (mSessionCallback != null) { mSessionCallback.onRequestSigning(signingId, algorithm, alias, data); } } catch (RemoteException e) { Log.w(TAG, "error in requestSigning", e); } } }); } /** * Requests signing of the given data. * *

This is used when the corresponding server of the broadcast-independent interactive * app requires signing during handshaking, and the interactive app service doesn't have * the built-in private key. The private key is provided by the content providers and * pre-built in the related app, such as TV app. * * @param signingId the ID to identify the request. When a result is received, this ID can * be used to correlate the result with the request. * @param algorithm the standard name of the signature algorithm requested, such as * MD5withRSA, SHA256withDSA, etc. The name is from standards like * FIPS PUB 186-4 and PKCS #1. * @param host the host of the SSL client authentication server. * @param port the port of the SSL client authentication server. * @param data the original bytes to be signed. * * @see #onSigningResult(String, byte[]) * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle) * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS */ @CallSuper @FlaggedApi(Flags.FLAG_TIAF_V_APIS) public void requestSigning(@NonNull String signingId, @NonNull String algorithm, @NonNull String host, int port, @NonNull byte[] data) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestSigning"); } if (mSessionCallback != null) { mSessionCallback.onRequestSigning2(signingId, algorithm, host, port, data); } } catch (RemoteException e) { Log.w(TAG, "error in requestSigning", e); } } }); } /** * Requests a SSL certificate for client validation. * * @param host the host name of the SSL authentication server. * @param port the port of the SSL authentication server. E.g., 443 */ @CallSuper @FlaggedApi(Flags.FLAG_TIAF_V_APIS) public void requestCertificate(@NonNull String host, int port) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestCertificate"); } if (mSessionCallback != null) { mSessionCallback.onRequestCertificate(host, port); } } catch (RemoteException e) { Log.w(TAG, "error in requestCertificate", e); } } }); } /** * Sends an advertisement request to be processed by the related TV input. * * @param request The advertisement request */ @CallSuper public void requestAd(@NonNull final AdRequest request) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "requestAd (id=" + request.getId() + ")"); } if (mSessionCallback != null) { mSessionCallback.onAdRequest(request); } } catch (RemoteException e) { Log.w(TAG, "error in requestAd", e); } } }); } void startInteractiveApp() { onStartInteractiveApp(); } void stopInteractiveApp() { onStopInteractiveApp(); } void resetInteractiveApp() { onResetInteractiveApp(); } void createBiInteractiveApp(@NonNull Uri biIAppUri, @Nullable Bundle params) { onCreateBiInteractiveAppRequest(biIAppUri, params); } void destroyBiInteractiveApp(@NonNull String biIAppId) { onDestroyBiInteractiveAppRequest(biIAppId); } void setTeletextAppEnabled(boolean enable) { onSetTeletextAppEnabled(enable); } void sendCurrentVideoBounds(@NonNull Rect bounds) { onCurrentVideoBounds(bounds); } void sendCurrentChannelUri(@Nullable Uri channelUri) { onCurrentChannelUri(channelUri); } void sendCurrentChannelLcn(int lcn) { onCurrentChannelLcn(lcn); } void sendStreamVolume(float volume) { onStreamVolume(volume); } void sendTrackInfoList(@NonNull List tracks) { onTrackInfoList(tracks); } void sendCurrentTvInputId(@Nullable String inputId) { onCurrentTvInputId(inputId); } void sendTimeShiftMode(int mode) { onTimeShiftMode(mode); } void sendAvailableSpeeds(@NonNull float[] speeds) { onAvailableSpeeds(speeds); } void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { onTvRecordingInfo(recordingInfo); } void sendTvRecordingInfoList(@Nullable List recordingInfoList) { onTvRecordingInfoList(recordingInfoList); } void sendSigningResult(String signingId, byte[] result) { onSigningResult(signingId, result); } void sendCertificate(String host, int port, Bundle certBundle) { SslCertificate cert = SslCertificate.restoreState(certBundle); onCertificate(host, port, cert); } void notifyError(String errMsg, Bundle params) { onError(errMsg, params); } void release() { onRelease(); if (mSurface != null) { mSurface.release(); mSurface = null; } synchronized (mLock) { mSessionCallback = null; mPendingActions.clear(); } // Removes the media view lastly so that any hanging on the main thread can be handled // in {@link #scheduleMediaViewCleanup}. removeMediaView(true); } void notifyTuned(Uri channelUri) { if (DEBUG) { Log.d(TAG, "notifyTuned (channelUri=" + channelUri + ")"); } onTuned(channelUri); } void notifyTrackSelected(int type, String trackId) { if (DEBUG) { Log.d(TAG, "notifyTrackSelected (type=" + type + "trackId=" + trackId + ")"); } // TvInputService accepts a Null String, but onTrackSelected expects NonNull. if (trackId == null) { trackId = ""; } onTrackSelected(type, trackId); } void notifyTracksChanged(List tracks) { if (DEBUG) { Log.d(TAG, "notifyTracksChanged (tracks=" + tracks + ")"); } onTracksChanged(tracks); } void notifyVideoAvailable() { if (DEBUG) { Log.d(TAG, "notifyVideoAvailable"); } onVideoAvailable(); } void notifyVideoUnavailable(int reason) { if (DEBUG) { Log.d(TAG, "notifyVideoAvailable (reason=" + reason + ")"); } onVideoUnavailable(reason); } void notifyVideoFreezeUpdated(boolean isFrozen) { if (DEBUG) { Log.d(TAG, "notifyVideoFreezeUpdated (isFrozen=" + isFrozen + ")"); } onVideoFreezeUpdated(isFrozen); } void notifyContentAllowed() { if (DEBUG) { Log.d(TAG, "notifyContentAllowed"); } onContentAllowed(); } void notifyContentBlocked(TvContentRating rating) { if (DEBUG) { Log.d(TAG, "notifyContentBlocked (rating=" + rating.flattenToString() + ")"); } onContentBlocked(rating); } void notifySignalStrength(int strength) { if (DEBUG) { Log.d(TAG, "notifySignalStrength (strength=" + strength + ")"); } onSignalStrength(strength); } /** * Calls {@link #onBroadcastInfoResponse}. */ void notifyBroadcastInfoResponse(BroadcastInfoResponse response) { if (DEBUG) { Log.d(TAG, "notifyBroadcastInfoResponse (requestId=" + response.getRequestId() + ")"); } onBroadcastInfoResponse(response); } /** * Calls {@link #onAdResponse}. */ void notifyAdResponse(AdResponse response) { if (DEBUG) { Log.d(TAG, "notifyAdResponse (requestId=" + response.getId() + ")"); } onAdResponse(response); } void notifyTvMessage(int type, Bundle data) { if (DEBUG) { Log.d(TAG, "notifyTvMessage (type=" + type + ", data= " + data + ")"); } onTvMessage(type, data); } void sendSelectedTrackInfo(List tracks) { if (DEBUG) { Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")"); } onSelectedTrackInfo(tracks); } /** * Calls {@link #onAdBufferConsumed}. */ void notifyAdBufferConsumed(AdBuffer buffer) { if (DEBUG) { Log.d(TAG, "notifyAdBufferConsumed (buffer=" + buffer + ")"); } onAdBufferConsumed(buffer); } /** * Calls {@link #onRecordingStarted(String, String)}. */ void notifyRecordingStarted(String recordingId, String requestId) { onRecordingStarted(recordingId, requestId); } /** * Calls {@link #onRecordingStopped(String)}. */ void notifyRecordingStopped(String recordingId) { onRecordingStopped(recordingId); } /** * Calls {@link #onRecordingConnectionFailed(String, String)}. */ void notifyRecordingConnectionFailed(String recordingId, String inputId) { onRecordingConnectionFailed(recordingId, inputId); } /** * Calls {@link #onRecordingDisconnected(String, String)}. */ void notifyRecordingDisconnected(String recordingId, String inputId) { onRecordingDisconnected(recordingId, inputId); } /** * Calls {@link #onRecordingTuned(String, Uri)}. */ void notifyRecordingTuned(String recordingId, Uri channelUri) { onRecordingTuned(recordingId, channelUri); } /** * Calls {@link #onRecordingError(String, int)}. */ void notifyRecordingError(String recordingId, int err) { onRecordingError(recordingId, err); } /** * Calls {@link #onRecordingScheduled(String, String)}. */ void notifyRecordingScheduled(String recordingId, String requestId) { onRecordingScheduled(recordingId, requestId); } /** * Calls {@link #onTimeShiftPlaybackParams(PlaybackParams)}. */ void notifyTimeShiftPlaybackParams(PlaybackParams params) { onTimeShiftPlaybackParams(params); } /** * Calls {@link #onTimeShiftStatusChanged(String, int)}. */ void notifyTimeShiftStatusChanged(String inputId, int status) { onTimeShiftStatusChanged(inputId, status); } /** * Calls {@link #onTimeShiftStartPositionChanged(String, long)}. */ void notifyTimeShiftStartPositionChanged(String inputId, long timeMs) { onTimeShiftStartPositionChanged(inputId, timeMs); } /** * Calls {@link #onTimeShiftCurrentPositionChanged(String, long)}. */ void notifyTimeShiftCurrentPositionChanged(String inputId, long timeMs) { onTimeShiftCurrentPositionChanged(inputId, timeMs); } /** * Notifies when the session state is changed. * * @param state the current session state. * @param err the error code for error state. {@link TvInteractiveAppManager#ERROR_NONE} is * used when the state is not * {@link TvInteractiveAppManager#INTERACTIVE_APP_STATE_ERROR}. */ @CallSuper public void notifySessionStateChanged( @TvInteractiveAppManager.InteractiveAppState int state, @TvInteractiveAppManager.ErrorCode int err) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "notifySessionStateChanged (state=" + state + "; err=" + err + ")"); } if (mSessionCallback != null) { mSessionCallback.onSessionStateChanged(state, err); } } catch (RemoteException e) { Log.w(TAG, "error in notifySessionStateChanged", e); } } }); } /** * Notifies the broadcast-independent(BI) interactive application has been created. * * @param biIAppId BI interactive app ID, which can be used to destroy the BI interactive * app. {@code null} if it's not created successfully. * * @see #onCreateBiInteractiveAppRequest(Uri, Bundle) */ @CallSuper public final void notifyBiInteractiveAppCreated( @NonNull Uri biIAppUri, @Nullable String biIAppId) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "notifyBiInteractiveAppCreated (biIAppId=" + biIAppId + ")"); } if (mSessionCallback != null) { mSessionCallback.onBiInteractiveAppCreated(biIAppUri, biIAppId); } } catch (RemoteException e) { Log.w(TAG, "error in notifyBiInteractiveAppCreated", e); } } }); } /** * Notifies when the digital teletext app state is changed. * @param state the current state. */ @CallSuper public final void notifyTeletextAppStateChanged( @TvInteractiveAppManager.TeletextAppState int state) { executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "notifyTeletextAppState (state=" + state + ")"); } if (mSessionCallback != null) { mSessionCallback.onTeletextAppStateChanged(state); } } catch (RemoteException e) { Log.w(TAG, "error in notifyTeletextAppState", e); } } }); } /** * Notifies when the advertisement buffer is filled and ready to be read. * * @param buffer The {@link AdBuffer} to be received */ @CallSuper public void notifyAdBufferReady(@NonNull AdBuffer buffer) { AdBuffer dupBuffer; try { dupBuffer = AdBuffer.dupAdBuffer(buffer); } catch (IOException e) { Log.w(TAG, "dup AdBuffer error in notifyAdBufferReady:", e); return; } executeOrPostRunnableOnMainThread(new Runnable() { @MainThread @Override public void run() { try { if (DEBUG) { Log.d(TAG, "notifyAdBufferReady(buffer=" + buffer + ")"); } if (mSessionCallback != null) { mSessionCallback.onAdBufferReady(dupBuffer); } } catch (RemoteException e) { Log.w(TAG, "error in notifyAdBuffer", e); } finally { if (dupBuffer != null) { dupBuffer.getSharedMemory().close(); } } } }); } /** * 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 + ")"); if (event instanceof KeyEvent) { KeyEvent keyEvent = (KeyEvent) event; if (keyEvent.dispatch(this, mDispatcherState, this)) { return TvInteractiveAppManager.Session.DISPATCH_HANDLED; } // TODO: special handlings of navigation keys and media keys } else if (event instanceof MotionEvent) { MotionEvent motionEvent = (MotionEvent) event; final int source = motionEvent.getSource(); if (motionEvent.isTouchEvent()) { if (onTouchEvent(motionEvent)) { return TvInteractiveAppManager.Session.DISPATCH_HANDLED; } } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { if (onTrackballEvent(motionEvent)) { return TvInteractiveAppManager.Session.DISPATCH_HANDLED; } } else { if (onGenericMotionEvent(motionEvent)) { return TvInteractiveAppManager.Session.DISPATCH_HANDLED; } } } // TODO: handle overlay view return TvInteractiveAppManager.Session.DISPATCH_NOT_HANDLED; } private void initialize(ITvInteractiveAppSessionCallback callback) { synchronized (mLock) { mSessionCallback = callback; for (Runnable runnable : mPendingActions) { runnable.run(); } mPendingActions.clear(); } } /** * 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); } 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); } } } } /** * Creates an media view. This calls {@link #onCreateMediaView} to get a view to attach * to the media window. * * @param windowToken A window token of the application. * @param frame A position of the media view. */ void createMediaView(IBinder windowToken, Rect frame) { if (mMediaViewContainer != null) { removeMediaView(false); } if (DEBUG) Log.d(TAG, "create media view(" + frame + ")"); mWindowToken = windowToken; mMediaFrame = frame; onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); if (!mMediaViewEnabled) { return; } mMediaView = onCreateMediaView(); if (mMediaView == null) { return; } if (mMediaViewCleanUpTask != null) { mMediaViewCleanUpTask.cancel(true); mMediaViewCleanUpTask = null; } // Creates a container view to check hanging on the media view detaching. // Adding/removing the media view to/from the container make the view attach/detach // logic run on the main thread. mMediaViewContainer = new FrameLayout(mContext.getApplicationContext()); mMediaViewContainer.addView(mMediaView); int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; // 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(mMediaViewContainer, mWindowParams); } /** * Relayouts the current media view. * * @param frame A new position of the media view. */ void relayoutMediaView(Rect frame) { if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")"); if (mMediaFrame == null || mMediaFrame.width() != frame.width() || mMediaFrame.height() != frame.height()) { // Note: relayoutMediaView is called whenever TvInteractiveAppView's layout is // changed regardless of setMediaViewEnabled. onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); } mMediaFrame = frame; if (!mMediaViewEnabled || mMediaViewContainer == null) { return; } mWindowParams.x = frame.left; mWindowParams.y = frame.top; mWindowParams.width = frame.right - frame.left; mWindowParams.height = frame.bottom - frame.top; mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams); } /** * Removes the current media view. */ void removeMediaView(boolean clearWindowToken) { if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")"); if (clearWindowToken) { mWindowToken = null; mMediaFrame = null; } if (mMediaViewContainer != null) { // Removes the media view from the view hierarchy in advance so that it can be // cleaned up in the {@link MediaViewCleanUpTask} if the remove process is // hanging. mMediaViewContainer.removeView(mMediaView); mMediaView = null; mWindowManager.removeView(mMediaViewContainer); mMediaViewContainer = null; mWindowParams = null; } } /** * Schedules a task which checks whether the media 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 scheduleMediaViewCleanup() { View mediaViewParent = mMediaViewContainer; if (mediaViewParent != null) { mMediaViewCleanUpTask = new MediaViewCleanUpTask(); mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mediaViewParent); } } } private static final class MediaViewCleanUpTask extends AsyncTask { @Override protected Void doInBackground(View... views) { View mediaViewParent = views[0]; try { Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS); } catch (InterruptedException e) { return null; } if (isCancelled()) { return null; } if (mediaViewParent.isAttachedToWindow()) { Log.e(TAG, "Time out on releasing media view. Killing " + mediaViewParent.getContext().getPackageName()); android.os.Process.killProcess(Process.myPid()); } return null; } } @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_NOTIFY_RTE_STATE_CHANGED = 3; private void broadcastRteStateChanged(int type, int state, int error) { int n = mCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { try { mCallbacks.getBroadcastItem(i).onStateChanged(type, state, error); } catch (RemoteException e) { Log.e(TAG, "error in broadcastRteStateChanged", e); } } mCallbacks.finishBroadcast(); } @Override public void handleMessage(Message msg) { switch (msg.what) { case DO_CREATE_SESSION: { SomeArgs args = (SomeArgs) msg.obj; InputChannel channel = (InputChannel) args.arg1; ITvInteractiveAppSessionCallback cb = (ITvInteractiveAppSessionCallback) args.arg2; String iAppServiceId = (String) args.arg3; int type = (int) args.arg4; args.recycle(); Session sessionImpl = onCreateSession(iAppServiceId, type); if (sessionImpl == null) { try { // Failed to create a session. cb.onSessionCreated(null); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } return; } ITvInteractiveAppSession stub = new ITvInteractiveAppSessionWrapper( TvInteractiveAppService.this, sessionImpl, channel); SomeArgs someArgs = SomeArgs.obtain(); someArgs.arg1 = sessionImpl; someArgs.arg2 = stub; someArgs.arg3 = cb; 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; ITvInteractiveAppSession stub = (ITvInteractiveAppSession) args.arg2; ITvInteractiveAppSessionCallback cb = (ITvInteractiveAppSessionCallback) args.arg3; try { cb.onSessionCreated(stub); } catch (RemoteException e) { Log.e(TAG, "error in onSessionCreated", e); } if (sessionImpl != null) { sessionImpl.initialize(cb); } args.recycle(); return; } case DO_NOTIFY_RTE_STATE_CHANGED: { SomeArgs args = (SomeArgs) msg.obj; int type = (int) args.arg1; int state = (int) args.arg2; int error = (int) args.arg3; broadcastRteStateChanged(type, state, error); return; } default: { Log.w(TAG, "Unhandled message code: " + msg.what); return; } } } } }