diff options
18 files changed, 2095 insertions, 95 deletions
diff --git a/api/current.txt b/api/current.txt index da5bc5e6367b..230d22eb6dd7 100644 --- a/api/current.txt +++ b/api/current.txt @@ -22216,6 +22216,7 @@ package android.media.tv { method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri); method public static final android.net.Uri buildProgramsUriForChannel(long, long, long); method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long); + method public static final android.net.Uri buildRecordedProgramUri(long); field public static final java.lang.String AUTHORITY = "android.media.tv"; } @@ -22352,13 +22353,49 @@ package android.media.tv { field public static final java.lang.String TRAVEL = "TRAVEL"; } + public static final class TvContract.RecordedPrograms implements android.media.tv.TvContract.BaseTvColumns { + field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language"; + field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field public static final java.lang.String COLUMN_CANONICAL_GENRE = "canonical_genre"; + field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id"; + field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating"; + field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + field public static final java.lang.String COLUMN_EPISODE_NUMBER = "episode_number"; + field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4"; + field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description"; + field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri"; + field public static final java.lang.String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes"; + field public static final java.lang.String COLUMN_RECORDING_DATA_URI = "recording_data_uri"; + field public static final java.lang.String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis"; + field public static final java.lang.String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS = "recording_expire_time_utc_millis"; + field public static final java.lang.String COLUMN_SEARCHABLE = "searchable"; + field public static final java.lang.String COLUMN_SEASON_NUMBER = "season_number"; + field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description"; + field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri"; + field public static final java.lang.String COLUMN_TITLE = "title"; + field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number"; + field public static final java.lang.String COLUMN_VIDEO_HEIGHT = "video_height"; + field public static final java.lang.String COLUMN_VIDEO_WIDTH = "video_width"; + field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program"; + field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program"; + field public static final android.net.Uri CONTENT_URI; + } + public final class TvInputInfo implements android.os.Parcelable { + method public boolean canRecord(); method public android.content.Intent createSettingsIntent(); method public android.content.Intent createSetupIntent(); method public int describeContents(); method public java.lang.String getId(); method public java.lang.String getParentId(); method public android.content.pm.ServiceInfo getServiceInfo(); + method public int getTunerCount(); method public int getType(); method public boolean isPassthroughInput(); method public android.graphics.drawable.Drawable loadIcon(android.content.Context); @@ -22393,6 +22430,10 @@ package android.media.tv { field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1 field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2 field public static final java.lang.String META_DATA_CONTENT_RATING_SYSTEMS = "android.media.tv.metadata.CONTENT_RATING_SYSTEMS"; + field public static final int RECORDING_ERROR_CONNECTION_FAILED = 1; // 0x1 + field public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 2; // 0x2 + field public static final int RECORDING_ERROR_RESOURCE_BUSY = 3; // 0x3 + field public static final int RECORDING_ERROR_UNKNOWN = 0; // 0x0 field public static final long TIME_SHIFT_INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L field public static final int TIME_SHIFT_STATUS_AVAILABLE = 3; // 0x3 field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2 @@ -22410,12 +22451,15 @@ package android.media.tv { method public void onInputAdded(java.lang.String); method public void onInputRemoved(java.lang.String); method public void onInputStateChanged(java.lang.String, int); + method public void onTvInputInfoChanged(java.lang.String, android.media.tv.TvInputInfo); } public abstract class TvInputService extends android.app.Service { ctor public TvInputService(); method public final android.os.IBinder onBind(android.content.Intent); + method public abstract android.media.tv.TvInputService.RecordingSession onCreateRecordingSession(java.lang.String); method public abstract android.media.tv.TvInputService.Session onCreateSession(java.lang.String); + method public final void setTvInputInfo(java.lang.String, android.media.tv.TvInputInfo); field public static final java.lang.String SERVICE_INTERFACE = "android.media.tv.TvInputService"; field public static final java.lang.String SERVICE_META_DATA = "android.media.tv.input"; } @@ -22428,6 +22472,18 @@ package android.media.tv { method public final boolean onSetSurface(android.view.Surface); } + public static abstract class TvInputService.RecordingSession { + ctor public TvInputService.RecordingSession(android.content.Context); + method public void notifyConnected(); + method public void notifyError(int); + method public void notifyRecordingStarted(); + method public void notifyRecordingStopped(android.net.Uri); + method public abstract void onConnect(android.net.Uri); + method public abstract void onDisconnect(); + method public abstract void onStartRecording(); + method public abstract void onStopRecording(); + } + public static abstract class TvInputService.Session implements android.view.KeyEvent.Callback { ctor public TvInputService.Session(android.content.Context); method public void layoutSurface(int, int, int, int); @@ -22455,6 +22511,7 @@ package android.media.tv { method public long onTimeShiftGetCurrentPosition(); method public long onTimeShiftGetStartPosition(); method public void onTimeShiftPause(); + method public void onTimeShiftPlay(android.net.Uri); method public void onTimeShiftResume(); method public void onTimeShiftSeekTo(long); method public void onTimeShiftSetPlaybackParams(android.media.PlaybackParams); @@ -22465,6 +22522,23 @@ package android.media.tv { method public void setOverlayViewEnabled(boolean); } + public class TvRecordingClient { + ctor public TvRecordingClient(android.content.Context, java.lang.String, android.media.tv.TvRecordingClient.RecordingCallback, android.os.Handler); + method public void connect(java.lang.String, android.net.Uri); + method public void disconnect(); + method public void startRecording(); + method public void stopRecording(); + } + + public class TvRecordingClient.RecordingCallback { + ctor public TvRecordingClient.RecordingCallback(); + method public void onConnected(); + method public void onDisconnected(); + method public void onError(int); + method public void onRecordingStarted(); + method public void onRecordingStopped(android.net.Uri); + } + public final class TvTrackInfo implements android.os.Parcelable { method public int describeContents(); method public final int getAudioChannelCount(); @@ -22516,6 +22590,7 @@ package android.media.tv { method public void setStreamVolume(float); method public void setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback); method public void timeShiftPause(); + method public void timeShiftPlay(java.lang.String, android.net.Uri); method public void timeShiftResume(); method public void timeShiftSeekTo(long); method public void timeShiftSetPlaybackParams(android.media.PlaybackParams); diff --git a/api/system-current.txt b/api/system-current.txt index 3c652530b6fd..e4f42c05843f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -23643,6 +23643,7 @@ package android.media.tv { method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri); method public static final android.net.Uri buildProgramsUriForChannel(long, long, long); method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long); + method public static final android.net.Uri buildRecordedProgramUri(long); method public static final boolean isChannelUriForPassthroughInput(android.net.Uri); field public static final java.lang.String AUTHORITY = "android.media.tv"; } @@ -23783,6 +23784,40 @@ package android.media.tv { field public static final java.lang.String TRAVEL = "TRAVEL"; } + public static final class TvContract.RecordedPrograms implements android.media.tv.TvContract.BaseTvColumns { + field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language"; + field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field public static final java.lang.String COLUMN_CANONICAL_GENRE = "canonical_genre"; + field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id"; + field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating"; + field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + field public static final java.lang.String COLUMN_EPISODE_NUMBER = "episode_number"; + field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4"; + field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description"; + field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri"; + field public static final java.lang.String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes"; + field public static final java.lang.String COLUMN_RECORDING_DATA_URI = "recording_data_uri"; + field public static final java.lang.String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis"; + field public static final java.lang.String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS = "recording_expire_time_utc_millis"; + field public static final java.lang.String COLUMN_SEARCHABLE = "searchable"; + field public static final java.lang.String COLUMN_SEASON_NUMBER = "season_number"; + field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description"; + field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri"; + field public static final java.lang.String COLUMN_TITLE = "title"; + field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number"; + field public static final java.lang.String COLUMN_VIDEO_HEIGHT = "video_height"; + field public static final java.lang.String COLUMN_VIDEO_WIDTH = "video_width"; + field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program"; + field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program"; + field public static final android.net.Uri CONTENT_URI; + } + public static final class TvContract.WatchedPrograms implements android.media.tv.TvContract.BaseTvColumns { field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id"; field public static final java.lang.String COLUMN_DESCRIPTION = "description"; @@ -23831,6 +23866,7 @@ package android.media.tv { } public final class TvInputInfo implements android.os.Parcelable { + method public boolean canRecord(); method public android.content.Intent createSettingsIntent(); method public android.content.Intent createSetupIntent(); method public static android.media.tv.TvInputInfo createTvInputInfo(android.content.Context, android.content.pm.ResolveInfo, android.hardware.hdmi.HdmiDeviceInfo, java.lang.String, java.lang.String, android.net.Uri) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; @@ -23842,6 +23878,7 @@ package android.media.tv { method public java.lang.String getId(); method public java.lang.String getParentId(); method public android.content.pm.ServiceInfo getServiceInfo(); + method public int getTunerCount(); method public int getType(); method public boolean isConnectedToHdmiSwitch(); method public boolean isHardwareInput(); @@ -23876,6 +23913,7 @@ package android.media.tv { method public android.media.tv.TvInputManager.Hardware acquireTvInputHardware(int, android.media.tv.TvInputManager.HardwareCallback, android.media.tv.TvInputInfo); method public void addBlockedRating(android.media.tv.TvContentRating); method public boolean captureFrame(java.lang.String, android.view.Surface, android.media.tv.TvStreamConfig); + method public void createRecordingSession(java.lang.String, android.media.tv.TvInputManager.SessionCallback, android.os.Handler); method public void createSession(java.lang.String, android.media.tv.TvInputManager.SessionCallback, android.os.Handler); method public java.util.List<android.media.tv.TvStreamConfig> getAvailableTvStreamConfigList(java.lang.String); method public java.util.List<android.media.tv.TvContentRating> getBlockedRatings(); @@ -23899,6 +23937,10 @@ package android.media.tv { field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1 field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2 field public static final java.lang.String META_DATA_CONTENT_RATING_SYSTEMS = "android.media.tv.metadata.CONTENT_RATING_SYSTEMS"; + field public static final int RECORDING_ERROR_CONNECTION_FAILED = 1; // 0x1 + field public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 2; // 0x2 + field public static final int RECORDING_ERROR_RESOURCE_BUSY = 3; // 0x3 + field public static final int RECORDING_ERROR_UNKNOWN = 0; // 0x0 field public static final long TIME_SHIFT_INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L field public static final int TIME_SHIFT_STATUS_AVAILABLE = 3; // 0x3 field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2 @@ -23963,16 +24005,19 @@ package android.media.tv { method public void onInputRemoved(java.lang.String); method public void onInputStateChanged(java.lang.String, int); method public void onInputUpdated(java.lang.String); + method public void onTvInputInfoChanged(java.lang.String, android.media.tv.TvInputInfo); } public abstract class TvInputService extends android.app.Service { ctor public TvInputService(); method public final android.os.IBinder onBind(android.content.Intent); + method public abstract android.media.tv.TvInputService.RecordingSession onCreateRecordingSession(java.lang.String); method public abstract android.media.tv.TvInputService.Session onCreateSession(java.lang.String); method public android.media.tv.TvInputInfo onHardwareAdded(android.media.tv.TvInputHardwareInfo); method public java.lang.String onHardwareRemoved(android.media.tv.TvInputHardwareInfo); method public android.media.tv.TvInputInfo onHdmiDeviceAdded(android.hardware.hdmi.HdmiDeviceInfo); method public java.lang.String onHdmiDeviceRemoved(android.hardware.hdmi.HdmiDeviceInfo); + method public final void setTvInputInfo(java.lang.String, android.media.tv.TvInputInfo); field public static final java.lang.String SERVICE_INTERFACE = "android.media.tv.TvInputService"; field public static final java.lang.String SERVICE_META_DATA = "android.media.tv.input"; } @@ -23985,6 +24030,21 @@ package android.media.tv { method public final boolean onSetSurface(android.view.Surface); } + public static abstract class TvInputService.RecordingSession { + ctor public TvInputService.RecordingSession(android.content.Context); + method public void notifyConnected(); + method public void notifyError(int); + method public void notifyRecordingStarted(); + method public void notifyRecordingStopped(android.net.Uri); + method public void notifySessionEvent(java.lang.String, android.os.Bundle); + method public void onAppPrivateCommand(java.lang.String, android.os.Bundle); + method public abstract void onConnect(android.net.Uri); + method public void onConnect(android.net.Uri, android.os.Bundle); + method public abstract void onDisconnect(); + method public abstract void onStartRecording(); + method public abstract void onStopRecording(); + } + public static abstract class TvInputService.Session implements android.view.KeyEvent.Callback { ctor public TvInputService.Session(android.content.Context); method public void layoutSurface(int, int, int, int); @@ -24015,6 +24075,7 @@ package android.media.tv { method public long onTimeShiftGetCurrentPosition(); method public long onTimeShiftGetStartPosition(); method public void onTimeShiftPause(); + method public void onTimeShiftPlay(android.net.Uri); method public void onTimeShiftResume(); method public void onTimeShiftSeekTo(long); method public void onTimeShiftSetPlaybackParams(android.media.PlaybackParams); @@ -24026,6 +24087,26 @@ package android.media.tv { method public void setOverlayViewEnabled(boolean); } + public class TvRecordingClient { + ctor public TvRecordingClient(android.content.Context, java.lang.String, android.media.tv.TvRecordingClient.RecordingCallback, android.os.Handler); + method public void connect(java.lang.String, android.net.Uri); + method public void connect(java.lang.String, android.net.Uri, android.os.Bundle); + method public void disconnect(); + method public void sendAppPrivateCommand(java.lang.String, android.os.Bundle); + method public void startRecording(); + method public void stopRecording(); + } + + public class TvRecordingClient.RecordingCallback { + ctor public TvRecordingClient.RecordingCallback(); + method public void onConnected(); + method public void onDisconnected(); + method public void onError(int); + method public void onEvent(java.lang.String, java.lang.String, android.os.Bundle); + method public void onRecordingStarted(); + method public void onRecordingStopped(android.net.Uri); + } + public class TvStreamConfig implements android.os.Parcelable { method public int describeContents(); method public int getFlags(); @@ -24108,6 +24189,7 @@ package android.media.tv { method public void setZOrderMediaOverlay(boolean); method public void setZOrderOnTop(boolean); method public void timeShiftPause(); + method public void timeShiftPlay(java.lang.String, android.net.Uri); method public void timeShiftResume(); method public void timeShiftSeekTo(long); method public void timeShiftSetPlaybackParams(android.media.PlaybackParams); diff --git a/api/test-current.txt b/api/test-current.txt index f022d5fd0d59..1a9b469a991e 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -22224,6 +22224,7 @@ package android.media.tv { method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri); method public static final android.net.Uri buildProgramsUriForChannel(long, long, long); method public static final android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long); + method public static final android.net.Uri buildRecordedProgramUri(long); field public static final java.lang.String AUTHORITY = "android.media.tv"; } @@ -22360,13 +22361,49 @@ package android.media.tv { field public static final java.lang.String TRAVEL = "TRAVEL"; } + public static final class TvContract.RecordedPrograms implements android.media.tv.TvContract.BaseTvColumns { + field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language"; + field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field public static final java.lang.String COLUMN_CANONICAL_GENRE = "canonical_genre"; + field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id"; + field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating"; + field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; + field public static final java.lang.String COLUMN_EPISODE_NUMBER = "episode_number"; + field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3"; + field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4"; + field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description"; + field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri"; + field public static final java.lang.String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes"; + field public static final java.lang.String COLUMN_RECORDING_DATA_URI = "recording_data_uri"; + field public static final java.lang.String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis"; + field public static final java.lang.String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS = "recording_expire_time_utc_millis"; + field public static final java.lang.String COLUMN_SEARCHABLE = "searchable"; + field public static final java.lang.String COLUMN_SEASON_NUMBER = "season_number"; + field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description"; + field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis"; + field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri"; + field public static final java.lang.String COLUMN_TITLE = "title"; + field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number"; + field public static final java.lang.String COLUMN_VIDEO_HEIGHT = "video_height"; + field public static final java.lang.String COLUMN_VIDEO_WIDTH = "video_width"; + field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program"; + field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program"; + field public static final android.net.Uri CONTENT_URI; + } + public final class TvInputInfo implements android.os.Parcelable { + method public boolean canRecord(); method public android.content.Intent createSettingsIntent(); method public android.content.Intent createSetupIntent(); method public int describeContents(); method public java.lang.String getId(); method public java.lang.String getParentId(); method public android.content.pm.ServiceInfo getServiceInfo(); + method public int getTunerCount(); method public int getType(); method public boolean isPassthroughInput(); method public android.graphics.drawable.Drawable loadIcon(android.content.Context); @@ -22401,6 +22438,10 @@ package android.media.tv { field public static final int INPUT_STATE_CONNECTED_STANDBY = 1; // 0x1 field public static final int INPUT_STATE_DISCONNECTED = 2; // 0x2 field public static final java.lang.String META_DATA_CONTENT_RATING_SYSTEMS = "android.media.tv.metadata.CONTENT_RATING_SYSTEMS"; + field public static final int RECORDING_ERROR_CONNECTION_FAILED = 1; // 0x1 + field public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 2; // 0x2 + field public static final int RECORDING_ERROR_RESOURCE_BUSY = 3; // 0x3 + field public static final int RECORDING_ERROR_UNKNOWN = 0; // 0x0 field public static final long TIME_SHIFT_INVALID_TIME = -9223372036854775808L; // 0x8000000000000000L field public static final int TIME_SHIFT_STATUS_AVAILABLE = 3; // 0x3 field public static final int TIME_SHIFT_STATUS_UNAVAILABLE = 2; // 0x2 @@ -22418,12 +22459,15 @@ package android.media.tv { method public void onInputAdded(java.lang.String); method public void onInputRemoved(java.lang.String); method public void onInputStateChanged(java.lang.String, int); + method public void onTvInputInfoChanged(java.lang.String, android.media.tv.TvInputInfo); } public abstract class TvInputService extends android.app.Service { ctor public TvInputService(); method public final android.os.IBinder onBind(android.content.Intent); + method public abstract android.media.tv.TvInputService.RecordingSession onCreateRecordingSession(java.lang.String); method public abstract android.media.tv.TvInputService.Session onCreateSession(java.lang.String); + method public final void setTvInputInfo(java.lang.String, android.media.tv.TvInputInfo); field public static final java.lang.String SERVICE_INTERFACE = "android.media.tv.TvInputService"; field public static final java.lang.String SERVICE_META_DATA = "android.media.tv.input"; } @@ -22436,6 +22480,18 @@ package android.media.tv { method public final boolean onSetSurface(android.view.Surface); } + public static abstract class TvInputService.RecordingSession { + ctor public TvInputService.RecordingSession(android.content.Context); + method public void notifyConnected(); + method public void notifyError(int); + method public void notifyRecordingStarted(); + method public void notifyRecordingStopped(android.net.Uri); + method public abstract void onConnect(android.net.Uri); + method public abstract void onDisconnect(); + method public abstract void onStartRecording(); + method public abstract void onStopRecording(); + } + public static abstract class TvInputService.Session implements android.view.KeyEvent.Callback { ctor public TvInputService.Session(android.content.Context); method public void layoutSurface(int, int, int, int); @@ -22463,6 +22519,7 @@ package android.media.tv { method public long onTimeShiftGetCurrentPosition(); method public long onTimeShiftGetStartPosition(); method public void onTimeShiftPause(); + method public void onTimeShiftPlay(android.net.Uri); method public void onTimeShiftResume(); method public void onTimeShiftSeekTo(long); method public void onTimeShiftSetPlaybackParams(android.media.PlaybackParams); @@ -22473,6 +22530,23 @@ package android.media.tv { method public void setOverlayViewEnabled(boolean); } + public class TvRecordingClient { + ctor public TvRecordingClient(android.content.Context, java.lang.String, android.media.tv.TvRecordingClient.RecordingCallback, android.os.Handler); + method public void connect(java.lang.String, android.net.Uri); + method public void disconnect(); + method public void startRecording(); + method public void stopRecording(); + } + + public class TvRecordingClient.RecordingCallback { + ctor public TvRecordingClient.RecordingCallback(); + method public void onConnected(); + method public void onDisconnected(); + method public void onError(int); + method public void onRecordingStarted(); + method public void onRecordingStopped(android.net.Uri); + } + public final class TvTrackInfo implements android.os.Parcelable { method public int describeContents(); method public final int getAudioChannelCount(); @@ -22524,6 +22598,7 @@ package android.media.tv { method public void setStreamVolume(float); method public void setTimeShiftPositionCallback(android.media.tv.TvView.TimeShiftPositionCallback); method public void timeShiftPause(); + method public void timeShiftPlay(java.lang.String, android.net.Uri); method public void timeShiftResume(); method public void timeShiftSeekTo(long); method public void timeShiftSetPlaybackParams(android.media.PlaybackParams); diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl index 86c0e5dc920c..8ef5ca0f62d8 100644 --- a/media/java/android/media/tv/ITvInputClient.aidl +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -43,4 +43,10 @@ oneway interface ITvInputClient { void onTimeShiftStatusChanged(int status, int seq); void onTimeShiftStartPositionChanged(long timeMs, int seq); void onTimeShiftCurrentPositionChanged(long timeMs, int seq); + + // For the recording session + void onConnected(int seq); + void onRecordingStarted(int seq); + void onRecordingStopped(in Uri recordedProgramUri, int seq); + void onError(int error, int seq); } diff --git a/media/java/android/media/tv/ITvInputManager.aidl b/media/java/android/media/tv/ITvInputManager.aidl index f8057dbfc898..0febc162e5a7 100644 --- a/media/java/android/media/tv/ITvInputManager.aidl +++ b/media/java/android/media/tv/ITvInputManager.aidl @@ -55,7 +55,8 @@ interface ITvInputManager { void addBlockedRating(in String rating, int userId); void removeBlockedRating(in String rating, int userId); - void createSession(in ITvInputClient client, in String inputId, int seq, int userId); + void createSession(in ITvInputClient client, in String inputId, boolean isRecordingSession, + int seq, int userId); void releaseSession(in IBinder sessionToken, int userId); void setMainSession(in IBinder sessionToken, int userId); @@ -77,12 +78,18 @@ interface ITvInputManager { void unblockContent(in IBinder sessionToken, in String unblockedRating, int userId); + void timeShiftPlay(in IBinder sessionToken, in Uri recordedProgramUri, int userId); void timeShiftPause(in IBinder sessionToken, int userId); void timeShiftResume(in IBinder sessionToken, int userId); void timeShiftSeekTo(in IBinder sessionToken, long timeMs, int userId); void timeShiftSetPlaybackParams(in IBinder sessionToken, in PlaybackParams params, int userId); void timeShiftEnablePositionTracking(in IBinder sessionToken, boolean enable, int userId); + // For the recording session + void connect(in IBinder sessionToken, in Uri channelUri, in Bundle params, int userId); + void startRecording(in IBinder sessionToken, int userId); + void stopRecording(in IBinder sessionToken, int userId); + // For TV input hardware binding List<TvInputHardwareInfo> getHardwareList(); ITvInputHardware acquireTvInputHardware(int deviceId, in ITvInputHardwareCallback callback, diff --git a/media/java/android/media/tv/ITvInputManagerCallback.aidl b/media/java/android/media/tv/ITvInputManagerCallback.aidl index 67926807b156..3bf415ba4bc5 100644 --- a/media/java/android/media/tv/ITvInputManagerCallback.aidl +++ b/media/java/android/media/tv/ITvInputManagerCallback.aidl @@ -16,13 +16,18 @@ package android.media.tv; +import android.media.tv.TvInputInfo; + /** * Interface to receive callbacks from ITvInputManager regardless of sessions. * @hide */ oneway interface ITvInputManagerCallback { - void onInputStateChanged(in String inputId, int state); void onInputAdded(in String inputId); void onInputRemoved(in String inputId); void onInputUpdated(in String inputId); + + void onInputStateChanged(in String inputId, int state); + + void onTvInputInfoChanged(in String inputId, in TvInputInfo TvInputInfo); } diff --git a/media/java/android/media/tv/ITvInputService.aidl b/media/java/android/media/tv/ITvInputService.aidl index 7a853d1caa62..bd0518493557 100644 --- a/media/java/android/media/tv/ITvInputService.aidl +++ b/media/java/android/media/tv/ITvInputService.aidl @@ -27,10 +27,11 @@ import android.view.InputChannel; * @hide */ oneway interface ITvInputService { - void registerCallback(ITvInputServiceCallback callback); + void registerCallback(in ITvInputServiceCallback callback); void unregisterCallback(in ITvInputServiceCallback callback); - void createSession(in InputChannel channel, ITvInputSessionCallback callback, + void createSession(in InputChannel channel, in ITvInputSessionCallback callback, in String inputId); + void createRecordingSession(in ITvInputSessionCallback callback, in String inputId); // For hardware TvInputService void notifyHardwareAdded(in TvInputHardwareInfo hardwareInfo); diff --git a/media/java/android/media/tv/ITvInputServiceCallback.aidl b/media/java/android/media/tv/ITvInputServiceCallback.aidl index 74ab56215ff2..9f13882685aa 100644 --- a/media/java/android/media/tv/ITvInputServiceCallback.aidl +++ b/media/java/android/media/tv/ITvInputServiceCallback.aidl @@ -27,4 +27,6 @@ oneway interface ITvInputServiceCallback { void addHardwareTvInput(in int deviceId, in TvInputInfo inputInfo); void addHdmiTvInput(in int id, in TvInputInfo inputInfo); void removeTvInput(in String inputId); + + void setTvInputInfo(in String inputId, in TvInputInfo inputInfo); } diff --git a/media/java/android/media/tv/ITvInputSession.aidl b/media/java/android/media/tv/ITvInputSession.aidl index 6a06b8f19466..408a76277845 100644 --- a/media/java/android/media/tv/ITvInputSession.aidl +++ b/media/java/android/media/tv/ITvInputSession.aidl @@ -48,9 +48,16 @@ oneway interface ITvInputSession { void unblockContent(in String unblockedRating); + void timeShiftPlay(in Uri recordedProgramUri); void timeShiftPause(); void timeShiftResume(); void timeShiftSeekTo(long timeMs); void timeShiftSetPlaybackParams(in PlaybackParams params); void timeShiftEnablePositionTracking(boolean enable); + + // For the recording session + void connect(in Uri channelUri, in Bundle params); + void disconnect(); + void startRecording(); + void stopRecording(); } diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl index e93681086159..cb6a05e07b0d 100644 --- a/media/java/android/media/tv/ITvInputSessionCallback.aidl +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -40,4 +40,10 @@ oneway interface ITvInputSessionCallback { void onTimeShiftStatusChanged(int status); void onTimeShiftStartPositionChanged(long timeMs); void onTimeShiftCurrentPositionChanged(long timeMs); + + // For the recording session + void onConnected(); + void onRecordingStarted(); + void onRecordingStopped(in Uri recordedProgramUri); + void onError(int error); } diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java index f8c6f3fb7198..4ac58766ca94 100644 --- a/media/java/android/media/tv/ITvInputSessionWrapper.java +++ b/media/java/android/media/tv/ITvInputSessionWrapper.java @@ -59,20 +59,29 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand private static final int DO_RELAYOUT_OVERLAY_VIEW = 11; private static final int DO_REMOVE_OVERLAY_VIEW = 12; private static final int DO_UNBLOCK_CONTENT = 13; - private static final int DO_TIME_SHIFT_PAUSE = 14; - private static final int DO_TIME_SHIFT_RESUME = 15; - private static final int DO_TIME_SHIFT_SEEK_TO = 16; - private static final int DO_TIME_SHIFT_SET_PLAYBACK_PARAMS = 17; - private static final int DO_TIME_SHIFT_ENABLE_POSITION_TRACKING = 18; - + private static final int DO_TIME_SHIFT_PLAY = 14; + private static final int DO_TIME_SHIFT_PAUSE = 15; + private static final int DO_TIME_SHIFT_RESUME = 16; + private static final int DO_TIME_SHIFT_SEEK_TO = 17; + private static final int DO_TIME_SHIFT_SET_PLAYBACK_PARAMS = 18; + private static final int DO_TIME_SHIFT_ENABLE_POSITION_TRACKING = 19; + private static final int DO_CONNECT = 20; + private static final int DO_DISCONNECT = 21; + private static final int DO_START_RECORDING = 22; + private static final int DO_STOP_RECORDING = 23; + + private final boolean mIsRecordingSession; private final HandlerCaller mCaller; private TvInputService.Session mTvInputSessionImpl; + private TvInputService.RecordingSession mTvInputRecordingSessionImpl; + private InputChannel mChannel; private TvInputEventReceiver mReceiver; public ITvInputSessionWrapper(Context context, TvInputService.Session sessionImpl, InputChannel channel) { + mIsRecordingSession = false; mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); mTvInputSessionImpl = sessionImpl; mChannel = channel; @@ -81,9 +90,19 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } } + public ITvInputSessionWrapper(Context context, + TvInputService.RecordingSession recordingSessionImpl) { + mIsRecordingSession = true; + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mTvInputRecordingSessionImpl = recordingSessionImpl; + } + @Override public void executeMessage(Message msg) { - if (mTvInputSessionImpl == null) { + if (!mIsRecordingSession && mTvInputSessionImpl == null) { + return; + } + if (mIsRecordingSession && mTvInputRecordingSessionImpl == null) { return; } @@ -138,7 +157,12 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } case DO_APP_PRIVATE_COMMAND: { SomeArgs args = (SomeArgs) msg.obj; - mTvInputSessionImpl.appPrivateCommand((String) args.arg1, (Bundle) args.arg2); + if (mIsRecordingSession) { + mTvInputRecordingSessionImpl.appPrivateCommand( + (String) args.arg1, (Bundle) args.arg2); + } else { + mTvInputSessionImpl.appPrivateCommand((String) args.arg1, (Bundle) args.arg2); + } args.recycle(); break; } @@ -160,6 +184,10 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mTvInputSessionImpl.unblockContent((String) msg.obj); break; } + case DO_TIME_SHIFT_PLAY: { + mTvInputSessionImpl.timeShiftPlay((Uri) msg.obj); + break; + } case DO_TIME_SHIFT_PAUSE: { mTvInputSessionImpl.timeShiftPause(); break; @@ -180,6 +208,25 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand mTvInputSessionImpl.timeShiftEnablePositionTracking((Boolean) msg.obj); break; } + case DO_CONNECT: { + SomeArgs args = (SomeArgs) msg.obj; + mTvInputRecordingSessionImpl.connect((Uri) args.arg1, (Bundle) args.arg2); + args.recycle(); + break; + } + case DO_DISCONNECT: { + mTvInputRecordingSessionImpl.disconnect(); + mTvInputRecordingSessionImpl = null; + break; + } + case DO_START_RECORDING: { + mTvInputRecordingSessionImpl.startRecording(); + break; + } + case DO_STOP_RECORDING: { + mTvInputRecordingSessionImpl.stopRecording(); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -274,6 +321,12 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand } @Override + public void timeShiftPlay(Uri recordedProgramUri) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO( + DO_TIME_SHIFT_PLAY, recordedProgramUri)); + } + + @Override public void timeShiftPause() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_TIME_SHIFT_PAUSE)); } @@ -300,6 +353,28 @@ public class ITvInputSessionWrapper extends ITvInputSession.Stub implements Hand DO_TIME_SHIFT_ENABLE_POSITION_TRACKING, enable)); } + @Override + public void connect(Uri channelUri, Bundle params) { + // Clear the pending connect requests. + mCaller.removeMessages(DO_CONNECT); + mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CONNECT, channelUri, params)); + } + + @Override + public void disconnect() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_DISCONNECT)); + } + + @Override + public void startRecording() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_START_RECORDING)); + } + + @Override + public void stopRecording() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_STOP_RECORDING)); + } + private final class TvInputEventReceiver extends InputEventReceiver { public TvInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index 7cd086ecf14d..62a01dcf8808 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -54,6 +54,7 @@ public final class TvContract { private static final String PATH_CHANNEL = "channel"; private static final String PATH_PROGRAM = "program"; + private static final String PATH_RECORDED_PROGRAM = "recorded_program"; private static final String PATH_PASSTHROUGH = "passthrough"; /** @@ -273,6 +274,15 @@ public final class TvContract { } /** + * Builds a URI that points to a specific recorded program. + * + * @param recordedProgramId The ID of the recorded program to point to. + */ + public static final Uri buildRecordedProgramUri(long recordedProgramId) { + return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, recordedProgramId); + } + + /** * Builds a URI that points to a specific program the user watched. * * @param watchedProgramId The ID of the watched program to point to. @@ -941,6 +951,8 @@ public final class TvContract { * * <p>This is a part of the channel URI and matches to {@link BaseColumns#_ID}. * + * <p>This is a required field. + * * <p>Type: INTEGER (long) */ public static final String COLUMN_CHANNEL_ID = "channel_id"; @@ -1336,6 +1348,382 @@ public final class TvContract { } /** + * Column definitions for the recorded TV programs table. + * + * <p>By default, the query results will be sorted by {@link #COLUMN_START_TIME_UTC_MILLIS} in + * ascending order. + */ + public static final class RecordedPrograms implements BaseTvColumns { + + /** The content:// style URI for this table. */ + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + + PATH_RECORDED_PROGRAM); + + /** The MIME type of a directory of recorded TV programs. */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program"; + + /** The MIME type of a single recorded TV program. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program"; + + /** + * The ID of the TV channel that provided this recorded TV program. + * + * <p>This is a part of the channel URI and matches to {@link BaseColumns#_ID}. + * + * <p>This is a required field. + * + * <p>Type: INTEGER (long) + * @see Programs#COLUMN_CHANNEL_ID + */ + public static final String COLUMN_CHANNEL_ID = Programs.COLUMN_CHANNEL_ID; + + /** + * The title of this recorded TV program. + * + * <p>If this recorded program is an episodic TV show, it is recommended that the title is + * the series title and its related fields ({@link #COLUMN_SEASON_NUMBER}, + * {@link #COLUMN_EPISODE_NUMBER}, and {@link #COLUMN_EPISODE_TITLE}) are filled in. + * + * <p>Type: TEXT + * @see Programs#COLUMN_TITLE + */ + public static final String COLUMN_TITLE = Programs.COLUMN_TITLE; + + /** + * The season number of this recorded TV program for episodic TV shows. + * + * <p>Can be empty. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_SEASON_NUMBER + */ + public static final String COLUMN_SEASON_NUMBER = Programs.COLUMN_SEASON_NUMBER; + + /** + * The episode number of this recorded TV program for episodic TV shows. + * + * <p>Can be empty. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_EPISODE_NUMBER + */ + public static final String COLUMN_EPISODE_NUMBER = Programs.COLUMN_EPISODE_NUMBER; + + /** + * The episode title of this recorded TV program for episodic TV shows. + * + * <p>Can be empty. + * + * <p>Type: TEXT + * @see Programs#COLUMN_EPISODE_TITLE + */ + public static final String COLUMN_EPISODE_TITLE = Programs.COLUMN_EPISODE_TITLE; + + /** + * The start time of the original TV program, in milliseconds since the epoch. + * + * <p>Type: INTEGER (long) + * @see Programs#COLUMN_START_TIME_UTC_MILLIS + */ + public static final String COLUMN_START_TIME_UTC_MILLIS = + Programs.COLUMN_START_TIME_UTC_MILLIS; + + /** + * The end time of the original TV program, in milliseconds since the epoch. + * + * <p>Type: INTEGER (long) + * @see Programs#COLUMN_END_TIME_UTC_MILLIS + */ + public static final String COLUMN_END_TIME_UTC_MILLIS = Programs.COLUMN_END_TIME_UTC_MILLIS; + + /** + * The comma-separated genre string of this recorded TV program. + * + * <p>Use the same language appeared in the underlying broadcast standard, if applicable. + * (For example, one can refer to the genre strings used in Genre Descriptor of ATSC A/65 or + * Content Descriptor of ETSI EN 300 468, if appropriate.) Otherwise, leave empty. + * + * <p>Type: TEXT + * @see Programs#COLUMN_BROADCAST_GENRE + */ + public static final String COLUMN_BROADCAST_GENRE = Programs.COLUMN_BROADCAST_GENRE; + + /** + * The comma-separated canonical genre string of this recorded TV program. + * + * <p>Canonical genres are defined in {@link Programs.Genres}. Use + * {@link Programs.Genres#encode Genres.encode()} to create a text that can be stored in + * this column. Use {@link Programs.Genres#decode Genres.decode()} to get the canonical + * genre strings from the text stored in this column. + * + * <p>Type: TEXT + * @see Programs#COLUMN_CANONICAL_GENRE + * @see Programs.Genres + */ + public static final String COLUMN_CANONICAL_GENRE = Programs.COLUMN_CANONICAL_GENRE; + + /** + * The short description of this recorded TV program that is displayed to the user by + * default. + * + * <p>It is recommended to limit the length of the descriptions to 256 characters. + * + * <p>Type: TEXT + * @see Programs#COLUMN_SHORT_DESCRIPTION + */ + public static final String COLUMN_SHORT_DESCRIPTION = Programs.COLUMN_SHORT_DESCRIPTION; + + /** + * The detailed, lengthy description of this recorded TV program that is displayed only when + * the user wants to see more information. + * + * <p>TV input services should leave this field empty if they have no additional details + * beyond {@link #COLUMN_SHORT_DESCRIPTION}. + * + * <p>Type: TEXT + * @see Programs#COLUMN_LONG_DESCRIPTION + */ + public static final String COLUMN_LONG_DESCRIPTION = Programs.COLUMN_LONG_DESCRIPTION; + + /** + * The width of the video for this recorded TV program, in the unit of pixels. + * + * <p>Together with {@link #COLUMN_VIDEO_HEIGHT} this is used to determine the video + * resolution of the current recorded TV program. Can be empty if it is not known or the + * recorded program does not convey any video. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_VIDEO_WIDTH + */ + public static final String COLUMN_VIDEO_WIDTH = Programs.COLUMN_VIDEO_WIDTH; + + /** + * The height of the video for this recorded TV program, in the unit of pixels. + * + * <p>Together with {@link #COLUMN_VIDEO_WIDTH} this is used to determine the video + * resolution of the current recorded TV program. Can be empty if it is not known or the + * recorded program does not convey any video. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_VIDEO_HEIGHT + */ + public static final String COLUMN_VIDEO_HEIGHT = Programs.COLUMN_VIDEO_HEIGHT; + + /** + * The comma-separated audio languages of this recorded TV program. + * + * <p>This is used to describe available audio languages included in the recorded program. + * Use either ISO 639-1 or 639-2/T codes. + * + * <p>Type: TEXT + * @see Programs#COLUMN_AUDIO_LANGUAGE + */ + public static final String COLUMN_AUDIO_LANGUAGE = Programs.COLUMN_AUDIO_LANGUAGE; + + /** + * The comma-separated content ratings of this recorded TV program. + * + * <p>This is used to describe the content rating(s) of this recorded program. Each + * comma-separated content rating sub-string should be generated by calling + * {@link TvContentRating#flattenToString}. Note that in most cases the recorded program + * content is rated by a single rating system, thus resulting in a corresponding single + * sub-string that does not require comma separation and multiple sub-strings appear only + * when the recorded program content is rated by two or more content rating systems. If any + * of those ratings is specified as "blocked rating" in the user's parental control + * settings, the TV input service should block the current content and wait for the signal + * that it is okay to unblock. + * + * <p>Type: TEXT + * @see Programs#COLUMN_CONTENT_RATING + */ + public static final String COLUMN_CONTENT_RATING = Programs.COLUMN_CONTENT_RATING; + + /** + * The URI for the poster art of this recorded TV program. + * + * <p>The data in the column must be a URL, or a URI in one of the following formats: + * + * <ul> + * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> + * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) + * </li> + * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li> + * </ul> + * + * <p>Can be empty. + * + * <p>Type: TEXT + * @see Programs#COLUMN_POSTER_ART_URI + */ + public static final String COLUMN_POSTER_ART_URI = Programs.COLUMN_POSTER_ART_URI; + + /** + * The URI for the thumbnail of this recorded TV program. + * + * <p>The system can generate a thumbnail from the poster art if this column is not + * specified. Thus it is not necessary for TV input services to include a thumbnail if it is + * just a scaled image of the poster art. + * + * <p>The data in the column must be a URL, or a URI in one of the following formats: + * + * <ul> + * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li> + * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE}) + * </li> + * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li> + * </ul> + * + * <p>Can be empty. + * + * <p>Type: TEXT + * @see Programs#COLUMN_THUMBNAIL_URI + */ + public static final String COLUMN_THUMBNAIL_URI = Programs.COLUMN_THUMBNAIL_URI; + + /** + * The flag indicating whether this recorded TV program is searchable or not. + * + * <p>The columns of searchable recorded programs can be read by other applications that + * have proper permission. Care must be taken not to open sensitive data. + * + * <p>A value of 1 indicates that the recorded program is searchable and its columns can be + * read by other applications, a value of 0 indicates that the recorded program is hidden + * and its columns can be read only by the package that owns the recorded program and the + * system. If not specified, this value is set to 1 (searchable) by default. + * + * <p>Type: INTEGER (boolean) + * @see Programs#COLUMN_SEARCHABLE + */ + public static final String COLUMN_SEARCHABLE = Programs.COLUMN_SEARCHABLE; + + /** + * The URI of the recording data for this recorded program. + * + * <p>Together with {@link #COLUMN_RECORDING_DATA_BYTES}, applications can use this + * information to manage recording storage. The URI should indicate a file or directory with + * the scheme {@link android.content.ContentResolver#SCHEME_FILE}. + * + * <p>Type: TEXT + * @see #COLUMN_RECORDING_DATA_BYTES + */ + public static final String COLUMN_RECORDING_DATA_URI = "recording_data_uri"; + + /** + * The data size (in bytes) for this recorded program. + * + * <p>Together with {@link #COLUMN_RECORDING_DATA_URI}, applications can use this + * information to manage recording storage. + * + * <p>Type: INTEGER (long) + * @see #COLUMN_RECORDING_DATA_URI + */ + public static final String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes"; + + /** + * The duration (in milliseconds) of this recorded program. + * + * <p>The actual duration of the recorded program can differ from the one calculated by + * {@link #COLUMN_END_TIME_UTC_MILLIS} - {@link #COLUMN_START_TIME_UTC_MILLIS} as program + * recording can be interrupted in the middle for some reason, resulting in a partially + * recorded program, which is still playable. + * + * <p>Type: INTEGER + */ + public static final String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis"; + + /** + * The expiration time for this recorded program, in milliseconds since the epoch. + * + * <p>Recorded TV programs do not expire by default unless explicitly requested by the user + * or the user allows applications to delete them in order to free up disk space for future + * recording. However, some TV content can have expiration date set by the content provider + * when recorded. This field is used to indicate such a restriction. + * + * <p>Can be empty. + * + * <p>Type: INTEGER (long) + */ + public static final String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS = + "recording_expire_time_utc_millis"; + + + /** + * Internal data used by individual TV input services. + * + * <p>This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * + * <p>Type: BLOB + * @see Programs#COLUMN_INTERNAL_PROVIDER_DATA + */ + public static final String COLUMN_INTERNAL_PROVIDER_DATA = + Programs.COLUMN_INTERNAL_PROVIDER_DATA; + + /** + * Internal integer flag used by individual TV input services. + * + * <p>This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG1 + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 = + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1; + + /** + * Internal integer flag used by individual TV input services. + * + * <p>This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG2 + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 = + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2; + + /** + * Internal integer flag used by individual TV input services. + * + * <p>This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG3 + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 = + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3; + + /** + * Internal integer flag used by individual TV input services. + * + * <p>This is internal to the provider that inserted it, and should not be decoded by other + * apps. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG4 + */ + public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 = + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4; + + /** + * The version number of this row entry used by TV input services. + * + * <p>This is best used by sync adapters to identify the rows to update. The number can be + * defined by individual TV input services. One may assign the same value as + * {@code version_number} in ETSI EN 300 468 or ATSC A/65, if the data are coming from a TV + * broadcast. + * + * <p>Type: INTEGER + * @see Programs#COLUMN_VERSION_NUMBER + */ + public static final String COLUMN_VERSION_NUMBER = Programs.COLUMN_VERSION_NUMBER; + + private RecordedPrograms() {} + } + + /** * Column definitions for the TV programs that the user watched. Applications do not have access * to this table. * @@ -1376,6 +1764,8 @@ public final class TvContract { /** * The ID of the TV channel that provides this TV program. * + * <p>This is a required field. + * * <p>Type: INTEGER (long) */ public static final String COLUMN_CHANNEL_ID = "channel_id"; diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java index a3d748efafd2..396023079709 100644 --- a/media/java/android/media/tv/TvInputInfo.java +++ b/media/java/android/media/tv/TvInputInfo.java @@ -419,6 +419,27 @@ public final class TvInputInfo implements Parcelable { } /** + * Returns the number of tuners this TV input has. + * + * <p>This method is valid only for the input of type {@link #TYPE_TUNER}. + * + * <p>Tuners correspond to physical/logical resources that allow reception of TV signal. Having + * <i>N</i> tuners means that the TV input is capable of receiving <i>N</i> different channels + * concurrently. + * + */ + public int getTunerCount() { + return mType == TYPE_TUNER ? 1 : 0; + } + + /** + * Returns {@code true} if this TV input can record TV programs, {@code false} otherwise. + */ + public boolean canRecord() { + return false; + } + + /** * Returns the HDMI device information of this TV input. * @hide */ diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 6a13f826949a..f1de8fdc1817 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -44,6 +45,8 @@ import android.view.View; import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; @@ -126,6 +129,35 @@ public final class TvInputManager { public static final long TIME_SHIFT_INVALID_TIME = Long.MIN_VALUE; /** + * RecordingError when a requested operation cannot be completed due to a problem that does not + * fit under any other error code. + */ + public static final int RECORDING_ERROR_UNKNOWN = 0; + + /** + * RecordingError when an attempt to connect to a recording session has failed or the + * established connection has been disconnected without a known reason. + */ + public static final int RECORDING_ERROR_CONNECTION_FAILED = 1; + + /** + * RecordingError when recording cannot proceed due to insufficient storage space. + */ + public static final int RECORDING_ERROR_INSUFFICIENT_SPACE = 2; + + /** + * RecordingError when recording cannot proceed because the required recording resource is not + * able to be allocated. + */ + public static final int RECORDING_ERROR_RESOURCE_BUSY = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({RECORDING_ERROR_UNKNOWN, RECORDING_ERROR_CONNECTION_FAILED, + RECORDING_ERROR_INSUFFICIENT_SPACE, RECORDING_ERROR_RESOURCE_BUSY}) + public @interface RecordingError {} + + /** * The TV input is connected. * * <p>This state indicates that a source device is connected to the input port and is in the @@ -416,6 +448,39 @@ public final class TvInputManager { */ public void onTimeShiftCurrentPositionChanged(Session session, long timeMs) { } + + /** + * This is called when a recording session initiated by a call to {@link + * TvRecordingClient#connect(String, Uri)} has been established. + */ + void onConnected(Session session) { + } + + /** + * This is called when TV program recording on the current channel has started. + * + * @param session A {@link TvInputManager.Session} associated with this callback. + */ + void onRecordingStarted(Session session) { + } + + /** + * This is called when TV program recording on the current channel has stopped. The passed + * URI contains information about the new recorded program. + * + * @param recordedProgramUri The URI for the new recorded program. + * @see android.media.tv.TvContract.RecordedPrograms + **/ + void onRecordingStopped(Session session, Uri recordedProgramUri) { + } + + /** + * This is called when an issue has occurred before or during recording. + * + * @param error The error code. + */ + void onError(Session session, @TvInputManager.RecordingError int error) { + } } private static final class SessionCallbackRecord { @@ -565,6 +630,46 @@ public final class TvInputManager { } }); } + + // For the recording session only + void postConnected() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onConnected(mSession); + } + }); + } + + // For the recording session only + void postRecordingStarted() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRecordingStarted(mSession); + } + }); + } + + // For the recording session only + void postRecordingStopped(final Uri recordedProgramUri) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRecordingStopped(mSession, recordedProgramUri); + } + }); + } + + // For the recording session only + void postError(final int error) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onError(mSession, error); + } + }); + } } /** @@ -574,7 +679,7 @@ public final class TvInputManager { /** * This is called when the state of a given TV input is changed. * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. * @param state State of the TV input. The value is one of the following: * <ul> * <li>{@link TvInputManager#INPUT_STATE_CONNECTED} @@ -591,7 +696,7 @@ public final class TvInputManager { * <p>Normally it happens when the user installs a new TV input package that implements * {@link TvInputService} interface. * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. */ public void onInputAdded(String inputId) { } @@ -602,7 +707,7 @@ public final class TvInputManager { * <p>Normally it happens when the user uninstalls the previously installed TV input * package. * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. */ public void onInputRemoved(String inputId) { } @@ -613,12 +718,21 @@ public final class TvInputManager { * <p>Normally it happens when a previously installed TV input package is re-installed or * the media on which a newer version of the package exists becomes available/unavailable. * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. * @hide */ @SystemApi public void onInputUpdated(String inputId) { } + + /** + * This is called when the information about a given TV input is changed. + * + * @param inputId The ID of the TV input. + * @param inputInfo TvInputInfo object that contains the information about the TV input. + */ + public void onTvInputInfoChanged(String inputId, TvInputInfo inputInfo) { + } } private static final class TvInputCallbackRecord { @@ -634,38 +748,47 @@ public final class TvInputManager { return mCallback; } - public void postInputStateChanged(final String inputId, final int state) { + public void postInputAdded(final String inputId) { mHandler.post(new Runnable() { @Override public void run() { - mCallback.onInputStateChanged(inputId, state); + mCallback.onInputAdded(inputId); } }); } - public void postInputAdded(final String inputId) { + public void postInputRemoved(final String inputId) { mHandler.post(new Runnable() { @Override public void run() { - mCallback.onInputAdded(inputId); + mCallback.onInputRemoved(inputId); } }); } - public void postInputRemoved(final String inputId) { + public void postInputUpdated(final String inputId) { mHandler.post(new Runnable() { @Override public void run() { - mCallback.onInputRemoved(inputId); + mCallback.onInputUpdated(inputId); } }); } - public void postInputUpdated(final String inputId) { + public void postInputStateChanged(final String inputId, final int state) { mHandler.post(new Runnable() { @Override public void run() { - mCallback.onInputUpdated(inputId); + mCallback.onInputStateChanged(inputId, state); + } + }); + } + + public void postTvInputInfoChanged(final String inputId, final TvInputInfo inputInfo) { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onTvInputInfoChanged(inputId, inputInfo); } }); } @@ -876,19 +999,57 @@ public final class TvInputManager { record.postTimeShiftCurrentPositionChanged(timeMs); } } - }; - ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() { + @Override - public void onInputStateChanged(String inputId, int state) { - synchronized (mLock) { - mStateMap.put(inputId, state); - for (TvInputCallbackRecord record : mCallbackRecords) { - record.postInputStateChanged(inputId, state); + public void onConnected(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postConnected(); + } + } + + @Override + public void onRecordingStarted(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRecordingStarted(); + } + } + + @Override + public void onRecordingStopped(Uri recordedProgramUri, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; } + record.postRecordingStopped(recordedProgramUri); } } @Override + public void onError(int error, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postError(error); + } + } + }; + ITvInputManagerCallback managerCallback = new ITvInputManagerCallback.Stub() { + @Override public void onInputAdded(String inputId) { synchronized (mLock) { mStateMap.put(inputId, INPUT_STATE_CONNECTED); @@ -916,6 +1077,25 @@ public final class TvInputManager { } } } + + @Override + public void onInputStateChanged(String inputId, int state) { + synchronized (mLock) { + mStateMap.put(inputId, state); + for (TvInputCallbackRecord record : mCallbackRecords) { + record.postInputStateChanged(inputId, state); + } + } + } + + @Override + public void onTvInputInfoChanged(String inputId, TvInputInfo inputInfo) { + synchronized (mLock) { + for (TvInputCallbackRecord record : mCallbackRecords) { + record.postTvInputInfoChanged(inputId, inputInfo); + } + } + } }; try { if (mService != null) { @@ -972,7 +1152,7 @@ public final class TvInputManager { * <li>{@link #INPUT_STATE_DISCONNECTED} * </ul> * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. * @throws IllegalArgumentException if the argument is {@code null}. */ public int getInputState(@NonNull String inputId) { @@ -1139,7 +1319,7 @@ public final class TvInputManager { * <p>The number of sessions that can be created at the same time is limited by the capability * of the given TV input. * - * @param inputId The id of the TV input. + * @param inputId The ID of the TV input. * @param callback A callback used to receive the created session. * @param handler A {@link Handler} that the session creation will be delivered to. * @hide @@ -1147,6 +1327,28 @@ public final class TvInputManager { @SystemApi public void createSession(@NonNull String inputId, @NonNull final SessionCallback callback, @NonNull Handler handler) { + createSessionInternal(inputId, false, callback, handler); + } + + /** + * Creates a recording {@link Session} for a given TV input. + * + * <p>The number of sessions that can be created at the same time is limited by the capability + * of the given TV input. + * + * @param inputId The ID of the TV input. + * @param callback A callback used to receive the created session. + * @param handler A {@link Handler} that the session creation will be delivered to. + * @hide + */ + @SystemApi + public void createRecordingSession(@NonNull String inputId, + @NonNull final SessionCallback callback, @NonNull Handler handler) { + createSessionInternal(inputId, true, callback, handler); + } + + private void createSessionInternal(String inputId, boolean isRecordingSession, + SessionCallback callback, Handler handler) { Preconditions.checkNotNull(inputId); Preconditions.checkNotNull(callback); Preconditions.checkNotNull(handler); @@ -1155,7 +1357,7 @@ public final class TvInputManager { int seq = mNextSeq++; mSessionCallbackRecordMap.put(seq, record); try { - mService.createSession(mClient, inputId, seq, mUserId); + mService.createSession(mClient, inputId, isRecordingSession, seq, mUserId); } catch (RemoteException e) { throw new RuntimeException(e); } @@ -1171,7 +1373,7 @@ public final class TvInputManager { * here. This method is designed to be used with {@link #captureFrame} in * capture scenarios specifically and not suitable for any other use. * - * @param inputId the id of the TV input. + * @param inputId The ID of the TV input. * @return List of {@link TvStreamConfig} which is available for capturing * of the given TV input. * @hide @@ -1188,7 +1390,7 @@ public final class TvInputManager { /** * Take a snapshot of the given TV input into the provided Surface. * - * @param inputId the id of the TV input. + * @param inputId The ID of the TV input. * @param surface the {@link Surface} to which the snapshot is captured. * @param config the {@link TvStreamConfig} which is used for capturing. * @return true when the {@link Surface} is ready to be captured. @@ -1607,7 +1809,7 @@ public final class TvInputManager { * Returns the selected track for a given type. Returns {@code null} if the information is * not available or any of the tracks for the given type is not selected. * - * @return the ID of the selected track. + * @return The ID of the selected track. * @see #selectTrack */ @Nullable @@ -1697,6 +1899,21 @@ public final class TvInputManager { } /** + * Plays a given recorded TV program. + */ + void timeShiftPlay(Uri recordedProgramUri) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.timeShiftPlay(mToken, recordedProgramUri, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** * Pauses the playback. Call {@link #timeShiftResume()} to restart the playback. */ void timeShiftPause() { @@ -1782,6 +1999,62 @@ public final class TvInputManager { } /** + * Connects to a given channel for TV program recording. + */ + void connect(Uri channelUri) { + connect(channelUri, null); + } + + /** + * Tunes to a given channel. + * + * @param channelUri The URI of a channel. + * @param params Extra parameters. + */ + void connect(@NonNull Uri channelUri, Bundle params) { + Preconditions.checkNotNull(channelUri); + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.connect(mToken, channelUri, params, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Starts TV program recording for the current recording session. + */ + void startRecording() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.startRecording(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** + * Stops TV program recording for the current recording session. + */ + void stopRecording() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.stopRecording(mToken, mUserId); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** * Calls {@link TvInputService.Session#appPrivateCommand(String, Bundle) * TvInputService.Session.appPrivateCommand()} on the current TvView. * diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 053d43b6b69b..6d9b1ad339a4 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -136,6 +137,18 @@ public abstract class TvInputService extends Service { } @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_TV_INPUT, hardwareInfo).sendToTarget(); @@ -174,6 +187,17 @@ public abstract class TvInputService extends Service { public abstract Session onCreateSession(String inputId); /** + * Returns a concrete implementation of {@link RecordingSession}. + * + * <p>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 abstract RecordingSession onCreateRecordingSession(String inputId); + + /** * 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. @@ -229,6 +253,25 @@ public abstract class TvInputService extends Service { return null; } + + /** + * Sets the TvInputInfo for this TV input. + * + * <p>The system service automatically creates the TvInputInfo for each TV input based on + * information collected from the AndroidManifest.xml, thus it is not necessary to call this + * method unless the TV input has additional information to pass such as ability to record and + * tuner count. + * + * @param inputId The ID of the TV input. + * @param inputInfo The TvInputInfo object that contains that new information. + */ + public final void setTvInputInfo(String inputId, TvInputInfo inputInfo) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = inputId; + args.arg2 = inputInfo; + mServiceHandler.obtainMessage(ServiceHandler.DO_SET_TV_INPUT_INFO, args).sendToTarget(); + } + private boolean isPassthroughInput(String inputId) { if (mTvInputManager == null) { mTvInputManager = (TvInputManager) getSystemService(Context.TV_INPUT_SERVICE); @@ -322,7 +365,7 @@ public abstract class TvInputService extends Service { @SystemApi public void notifySessionEvent(@NonNull final String eventType, final Bundle eventArgs) { Preconditions.checkNotNull(eventType); - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { @Override public void run() { try { @@ -347,7 +390,8 @@ public abstract class TvInputService extends Service { * @param channelUri The URI of the new channel. */ public void notifyChannelRetuned(final Uri channelUri) { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -387,7 +431,8 @@ public abstract class TvInputService extends Service { // TODO: Validate the track list. final List<TvTrackInfo> tracksCopy = new ArrayList<>(tracks); - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -417,7 +462,8 @@ public abstract class TvInputService extends Service { * @see #onSelectTrack */ public void notifyTrackSelected(final int type, final String trackId) { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -443,7 +489,8 @@ public abstract class TvInputService extends Service { * @see #notifyVideoUnavailable */ public void notifyVideoAvailable() { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -478,7 +525,8 @@ public abstract class TvInputService extends Service { || reason > TvInputManager.VIDEO_UNAVAILABLE_REASON_END) { throw new IllegalArgumentException("Unknown reason: " + reason); } - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -518,7 +566,8 @@ public abstract class TvInputService extends Service { * @see TvInputManager */ public void notifyContentAllowed() { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -562,7 +611,8 @@ public abstract class TvInputService extends Service { */ public void notifyContentBlocked(@NonNull final TvContentRating rating) { Preconditions.checkNotNull(rating); - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -603,7 +653,8 @@ public abstract class TvInputService extends Service { * </ul> */ public void notifyTimeShiftStatusChanged(final int status) { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -619,7 +670,8 @@ public abstract class TvInputService extends Service { } private void notifyTimeShiftStartPositionChanged(final long timeMs) { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -635,7 +687,8 @@ public abstract class TvInputService extends Service { } private void notifyTimeShiftCurrentPositionChanged(final long timeMs) { - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -665,7 +718,8 @@ public abstract class TvInputService extends Service { if (left > right || top > bottom) { throw new IllegalArgumentException("Invalid parameter"); } - executeOrPostRunnable(new Runnable() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread @Override public void run() { try { @@ -858,13 +912,28 @@ public abstract class TvInputService extends Service { } /** + * 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 #onTimeShiftResume - * @see #onTimeShiftSeekTo - * @see #onTimeShiftSetPlaybackParams - * @see #onTimeShiftGetStartPosition - * @see #onTimeShiftGetCurrentPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftResume() + * @see #onTimeShiftSeekTo(long) + * @see #onTimeShiftSetPlaybackParams(PlaybackParams) + * @see #onTimeShiftGetStartPosition() + * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftPause() { } @@ -872,11 +941,12 @@ public abstract class TvInputService extends Service { /** * Called when the application requests to resume playback. * - * @see #onTimeShiftPause - * @see #onTimeShiftSeekTo - * @see #onTimeShiftSetPlaybackParams - * @see #onTimeShiftGetStartPosition - * @see #onTimeShiftGetCurrentPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftPause() + * @see #onTimeShiftSeekTo(long) + * @see #onTimeShiftSetPlaybackParams(PlaybackParams) + * @see #onTimeShiftGetStartPosition() + * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftResume() { } @@ -888,11 +958,12 @@ public abstract class TvInputService extends Service { * not in the range. * * @param timeMs The time position to seek to, in milliseconds since the epoch. - * @see #onTimeShiftResume - * @see #onTimeShiftPause - * @see #onTimeShiftSetPlaybackParams - * @see #onTimeShiftGetStartPosition - * @see #onTimeShiftGetCurrentPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftResume() + * @see #onTimeShiftPause() + * @see #onTimeShiftSetPlaybackParams(PlaybackParams) + * @see #onTimeShiftGetStartPosition() + * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftSeekTo(long timeMs) { } @@ -905,11 +976,12 @@ public abstract class TvInputService extends Service { * parameters previously set. * * @param params The playback params. - * @see #onTimeShiftResume - * @see #onTimeShiftPause - * @see #onTimeShiftSeekTo - * @see #onTimeShiftGetStartPosition - * @see #onTimeShiftGetCurrentPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftResume() + * @see #onTimeShiftPause() + * @see #onTimeShiftSeekTo(long) + * @see #onTimeShiftGetStartPosition() + * @see #onTimeShiftGetCurrentPosition() */ public void onTimeShiftSetPlaybackParams(PlaybackParams params) { } @@ -925,11 +997,12 @@ public abstract class TvInputService extends Service { * seek to, thus failure to notifying its change immediately might result in bad experience * where the application allows the user to seek to an invalid time position. * - * @see #onTimeShiftResume - * @see #onTimeShiftPause - * @see #onTimeShiftSeekTo - * @see #onTimeShiftSetPlaybackParams - * @see #onTimeShiftGetCurrentPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftResume() + * @see #onTimeShiftPause() + * @see #onTimeShiftSeekTo(long) + * @see #onTimeShiftSetPlaybackParams(PlaybackParams) + * @see #onTimeShiftGetCurrentPosition() */ public long onTimeShiftGetStartPosition() { return TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -944,11 +1017,12 @@ public abstract class TvInputService extends Service { * playback position reported by {@link #onTimeShiftGetStartPosition}. Failure to notifying * the correct current position might lead to bad user experience. * - * @see #onTimeShiftResume - * @see #onTimeShiftPause - * @see #onTimeShiftSeekTo - * @see #onTimeShiftSetPlaybackParams - * @see #onTimeShiftGetStartPosition + * @see #onTimeShiftPlay(Uri) + * @see #onTimeShiftResume() + * @see #onTimeShiftPause() + * @see #onTimeShiftSeekTo(long) + * @see #onTimeShiftSetPlaybackParams(PlaybackParams) + * @see #onTimeShiftGetStartPosition() */ public long onTimeShiftGetCurrentPosition() { return TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -1263,6 +1337,14 @@ public abstract class TvInputService extends Service { } /** + * Calls {@link #onTimeShiftPlay(Uri)}. + */ + void timeShiftPlay(Uri recordedProgramUri) { + mCurrentPositionMs = 0; + onTimeShiftPlay(recordedProgramUri); + } + + /** * Calls {@link #onTimeShiftPause}. */ void timeShiftPause() { @@ -1385,7 +1467,7 @@ public abstract class TvInputService extends Service { } } - private void executeOrPostRunnable(Runnable action) { + private void executeOrPostRunnableOnMainThread(Runnable action) { synchronized(mLock) { if (mSessionCallback == null) { // The session is not initialized yet. @@ -1449,6 +1531,267 @@ public abstract class TvInputService extends Service { } /** + * 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<Runnable> mPendingActions = new ArrayList<>(); + + /** + * Creates a new Recording Session for TV program recording. + * + * @param context The context of the application + */ + public RecordingSession(Context context) { + mHandler = new Handler(context.getMainLooper()); + } + + /** + * Informs the application that recording session has been connected. + */ + public void notifyConnected() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "notifyConnected"); + if (mSessionCallback != null) { + mSessionCallback.onConnected(); + } + } catch (RemoteException e) { + Log.w(TAG, "error in notifyConnected", e); + } + } + }); + } + + /** + * Informs the application that recording has started. + */ + public void notifyRecordingStarted() { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "notifyRecordingStarted"); + if (mSessionCallback != null) { + mSessionCallback.onRecordingStarted(); + } + } catch (RemoteException e) { + Log.w(TAG, "error in notifyRecordingStarted", e); + } + } + }); + } + + /** + * Informs the application that recording has stopped successfully. Each TV input service + * should create a new data entry in the recorded programs table upon completion of the + * recording and send its URI. + * + * @param recordedProgramUri The URI of the new 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); + } + } + }); + } + + /** + * Sends an error to the application at any moment. + * + * @param error The error code. Should be one of the followings. + * <ul> + * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN} + * <li>{@link TvInputManager#RECORDING_ERROR_CONNECTION_FAILED} + * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE} + * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY} + * </ul> + */ + public void notifyError(@TvInputManager.RecordingError final int error) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) Log.d(TAG, "notifyError"); + if (mSessionCallback != null) { + mSessionCallback.onError(error); + } + } 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 recording session is connected. + * + * @param channelUri The URI of the channel. + */ + public abstract void onConnect(Uri channelUri); + + /** + * Called when the recording session is connected. + * + * @param channelUri The URI of the channel. + * @param params Extra parameters. + * @hide + */ + @SystemApi + public void onConnect(Uri channelUri, Bundle params) { + onConnect(channelUri); + } + + /** + * Called when the application requests to disconnect the current recording session. + */ + public abstract void onDisconnect(); + + /** + * Called when the application requests to start recording. Recording must start + * immediately. + * + * <p>The session must call either {@link #notifyRecordingStarted()} or + * {@link #notifyError(int)}}. + */ + public abstract void onStartRecording(); + + /** + * Called when the application requests to stop recording. Recording must stop immediately. + * + * <p>The session must call either {@link #notifyRecordingStopped(Uri)} or + * {@link #notifyError(int)}}. + */ + public abstract void onStopRecording(); + + /** + * 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 <em>must</em> be a scoped name, + * i.e. prefixed with a package name you own, so that different developers will + * not create conflicting commands. + * @param data Any data to include with the command. + * @hide + */ + @SystemApi + public void onAppPrivateCommand(@NonNull String action, Bundle data) { + } + + /** + * Calls {@link #onConnect(Uri, Bundle)}. + * + */ + void connect(Uri channelUri, Bundle params) { + onConnect(channelUri, params); + } + + /** + * Calls {@link #onDisconnect()}. + * + */ + void disconnect() { + onDisconnect(); + } + + /** + * Calls {@link #onStartRecording()}. + * + */ + void startRecording() { + onStartRecording(); + } + + /** + * 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. * @@ -1588,10 +1931,12 @@ public abstract class TvInputService extends Service { 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_ADD_HARDWARE_TV_INPUT = 3; - private static final int DO_REMOVE_HARDWARE_TV_INPUT = 4; - private static final int DO_ADD_HDMI_TV_INPUT = 5; - private static final int DO_REMOVE_HDMI_TV_INPUT = 6; + private static final int DO_CREATE_RECORDING_SESSION = 3; + private static final int DO_ADD_HARDWARE_TV_INPUT = 4; + private static final int DO_REMOVE_HARDWARE_TV_INPUT = 5; + private static final int DO_ADD_HDMI_TV_INPUT = 6; + private static final int DO_REMOVE_HDMI_TV_INPUT = 7; + private static final int DO_SET_TV_INPUT_INFO = 8; private void broadcastAddHardwareTvInput(int deviceId, TvInputInfo inputInfo) { int n = mCallbacks.beginBroadcast(); @@ -1629,6 +1974,18 @@ public abstract class TvInputService extends Service { mCallbacks.finishBroadcast(); } + private void broadcastSetTvInputInfo(String inputId, TvInputInfo inputInfo) { + int n = mCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + mCallbacks.getBroadcastItem(i).setTvInputInfo(inputId, inputInfo); + } catch (RemoteException e) { + Log.e(TAG, "error in broadcastSetTvInputInfo", e); + } + } + mCallbacks.finishBroadcast(); + } + @Override public final void handleMessage(Message msg) { switch (msg.what) { @@ -1704,6 +2061,31 @@ public abstract class TvInputService extends Service { 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_TV_INPUT: { TvInputHardwareInfo hardwareInfo = (TvInputHardwareInfo) msg.obj; TvInputInfo inputInfo = onHardwareAdded(hardwareInfo); @@ -1736,6 +2118,16 @@ public abstract class TvInputService extends Service { } return; } + case DO_SET_TV_INPUT_INFO: { + SomeArgs args = (SomeArgs) msg.obj; + String inputId = (String) args.arg1; + TvInputInfo inputInfo = (TvInputInfo) args.arg2; + if (inputInfo != null) { + broadcastSetTvInputInfo(inputId, inputInfo); + } + args.recycle(); + return; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); return; diff --git a/media/java/android/media/tv/TvRecordingClient.java b/media/java/android/media/tv/TvRecordingClient.java new file mode 100644 index 000000000000..865e00021d05 --- /dev/null +++ b/media/java/android/media/tv/TvRecordingClient.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * The public interface object used to interact with a specific TV input service for TV program + * recording. + */ +public class TvRecordingClient { + private static final String TAG = "TvRecordingClient"; + private static final boolean DEBUG = false; + + private final RecordingCallback mCallback; + private final Handler mHandler; + + private final TvInputManager mTvInputManager; + private TvInputManager.Session mSession; + private MySessionCallback mSessionCallback; + + private final Queue<Pair<String, Bundle>> mPendingAppPrivateCommands = new ArrayDeque<>(); + + /** + * Creates a new TvRecordingClient object. + * + * @param context The application context to create the TvRecordingClient with. + * @param tag A short name for debugging purposes. + * @param callback The callback to receive recording status changes. + * @param handler The handler to invoke the callback on. + */ + public TvRecordingClient(Context context, String tag, @NonNull RecordingCallback callback, + Handler handler) { + mCallback = callback; + mHandler = handler == null ? new Handler(Looper.getMainLooper()) : handler; + mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); + } + + /** + * Connects to a given input for TV program recording. This will create a new recording session + * from the TV input and establishes the connection between the application and the session. + * + * <p>The recording session will respond by calling + * {@link RecordingCallback#onConnected()} or {@link RecordingCallback#onError(int)}. + * + * @param inputId The ID of the TV input for the given channel. + * @param channelUri The URI of a channel. + */ + public void connect(String inputId, Uri channelUri) { + connect(inputId, channelUri, null); + } + + /** + * Connects to a given input for TV program recording. This will create a new recording session + * from the TV input and establishes the connection between the application and the session. + * + * <p>The recording session will respond by calling + * {@link RecordingCallback#onConnected()} or {@link RecordingCallback#onError(int)}. + * + * @param inputId The ID of the TV input for the given channel. + * @param channelUri The URI of a channel. + * @param params Extra parameters. + * @hide + */ + @SystemApi + public void connect(String inputId, Uri channelUri, Bundle params) { + if (DEBUG) Log.d(TAG, "connect(" + channelUri + ")"); + if (TextUtils.isEmpty(inputId)) { + throw new IllegalArgumentException("inputId cannot be null or an empty string"); + } + if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { + if (mSession != null) { + mSession.connect(channelUri, params); + } else { + mSessionCallback.mChannelUri = channelUri; + mSessionCallback.mConnectionParams = params; + } + } else { + resetInternal(); + mSessionCallback = new MySessionCallback(inputId, channelUri, params); + if (mTvInputManager != null) { + mTvInputManager.createRecordingSession(inputId, mSessionCallback, mHandler); + } + } + } + + /** + * Disconnects the established connection between the application and the recording session. + * + * <p>The recording session will respond by calling + * {@link RecordingCallback#onDisconnected()} or {@link RecordingCallback#onError(int)}. + */ + public void disconnect() { + if (DEBUG) Log.d(TAG, "disconnect()"); + resetInternal(); + } + + private void resetInternal() { + mSessionCallback = null; + mPendingAppPrivateCommands.clear(); + if (mSession != null) { + mSession.release(); + mSession = null; + } + } + + /** + * Starts TV program recording for the current recording session. It is expected that recording + * starts immediately after calling this method. + * + * <p>The recording session will respond by calling + * {@link RecordingCallback#onRecordingStarted()} or {@link RecordingCallback#onError(int)}. + */ + public void startRecording() { + if (mSession != null) { + mSession.startRecording(); + } + } + + /** + * Stops TV program recording for the current recording session. It is expected that recording + * stops immediately after calling this method. + * + * <p>The recording session will respond by calling + * {@link RecordingCallback#onRecordingStopped(Uri)} or {@link RecordingCallback#onError(int)}. + */ + public void stopRecording() { + if (mSession != null) { + mSession.stopRecording(); + } + } + + /** + * Calls {@link TvInputService.RecordingSession#appPrivateCommand(String, Bundle) + * TvInputService.RecordingSession.appPrivateCommand()} on the current TvView. + * + * @param action The name of the private command to send. This <em>must</em> be a scoped name, + * i.e. prefixed with a package name you own, so that different developers will not + * create conflicting commands. + * @param data An optional bundle to send with the command. + * @hide + */ + @SystemApi + public void sendAppPrivateCommand(@NonNull String action, Bundle data) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException("action cannot be null or an empty string"); + } + if (mSession != null) { + mSession.sendAppPrivateCommand(action, data); + } else { + Log.w(TAG, "sendAppPrivateCommand - session not yet created (action \"" + action + + "\" pending)"); + mPendingAppPrivateCommands.add(Pair.create(action, data)); + } + } + + /** + * Callback used to receive various status updates on the + * {@link android.media.tv.TvInputService.RecordingSession} + */ + public class RecordingCallback { + /** + * This is called when a recording session initiated by a call to + * {@link #connect(String, Uri)} has been established. + */ + public void onConnected() { + } + + /** + * This is called when the established connection between the application and the recording + * session has been disconnected. Disconnection can be initiated either by an explicit + * request (i.e. a call to {@link #disconnect()} or by an error on the TV input service + * side. + */ + public void onDisconnected() { + } + + /** + * This is called when TV program recording on the current channel has started. + */ + public void onRecordingStarted() { + } + + /** + * This is called when TV program recording on the current channel has stopped. The passed + * URI contains information about the new recorded program. + * + * @param recordedProgramUri The URI for the new recorded program. + * @see android.media.tv.TvContract.RecordedPrograms + */ + public void onRecordingStopped(Uri recordedProgramUri) { + } + + /** + * This is called when an issue has occurred before or during recording. If the TV input + * service cannot proceed recording due to this error, a call to {@link #onDisconnected()} + * is expected to follow. + * + * @param error The error code. Should be one of the followings. + * <ul> + * <li>{@link TvInputManager#RECORDING_ERROR_UNKNOWN} + * <li>{@link TvInputManager#RECORDING_ERROR_CONNECTION_FAILED} + * <li>{@link TvInputManager#RECORDING_ERROR_INSUFFICIENT_SPACE} + * <li>{@link TvInputManager#RECORDING_ERROR_RESOURCE_BUSY} + * </ul> + */ + public void onError(@TvInputManager.RecordingError int error) { + } + + /** + * This is invoked when a custom event from the bound TV input is sent to this client. + * + * @param inputId The ID of the TV input bound to this client. + * @param eventType The type of the event. + * @param eventArgs Optional arguments of the event. + * @hide + */ + @SystemApi + public void onEvent(String inputId, String eventType, Bundle eventArgs) { + } + } + + private class MySessionCallback extends TvInputManager.SessionCallback { + final String mInputId; + Uri mChannelUri; + Bundle mConnectionParams; + + MySessionCallback(String inputId, Uri channelUri, Bundle connectionParams) { + mInputId = inputId; + mChannelUri = channelUri; + mConnectionParams = connectionParams; + } + + @Override + public void onSessionCreated(TvInputManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionCreated()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionCreated - session already created"); + // This callback is obsolete. + if (session != null) { + session.release(); + } + return; + } + mSession = session; + if (session != null) { + // Sends the pending app private commands. + for (Pair<String, Bundle> command : mPendingAppPrivateCommands) { + mSession.sendAppPrivateCommand(command.first, command.second); + } + mPendingAppPrivateCommands.clear(); + mSession.connect(mChannelUri, mConnectionParams); + } else { + mSessionCallback = null; + mCallback.onError(TvInputManager.RECORDING_ERROR_CONNECTION_FAILED); + } + } + + @Override + public void onSessionReleased(TvInputManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionReleased()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionReleased - session not created"); + return; + } + mSessionCallback = null; + mSession = null; + mCallback.onDisconnected(); + } + + @Override + public void onRecordingStarted(TvInputManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onRecordingStarted()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRecordingStarted - session not created"); + return; + } + mCallback.onRecordingStarted(); + } + + @Override + public void onRecordingStopped(TvInputManager.Session session, Uri recordedProgramUri) { + if (DEBUG) { + Log.d(TAG, "onRecordingStopped()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRecordingStopped - session not created"); + return; + } + mCallback.onRecordingStopped(recordedProgramUri); + } + + @Override + public void onError(TvInputManager.Session session, int error) { + if (DEBUG) { + Log.d(TAG, "onError()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onError - session not created"); + return; + } + mCallback.onError(error); + } + + @Override + public void onSessionEvent(TvInputManager.Session session, String eventType, + Bundle eventArgs) { + if (DEBUG) { + Log.d(TAG, "onSessionEvent(" + eventType + ")"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionEvent - session not created"); + return; + } + if (mCallback != null) { + mCallback.onEvent(mInputId, eventType, eventArgs); + } + } + } +} diff --git a/media/java/android/media/tv/TvView.java b/media/java/android/media/tv/TvView.java index 003a2741e557..0132d2438470 100644 --- a/media/java/android/media/tv/TvView.java +++ b/media/java/android/media/tv/TvView.java @@ -448,6 +448,37 @@ public class TvView extends ViewGroup { } /** + * Plays a given recorded TV program. + * + * @param inputId The ID of the TV input that created the given recorded program. + * @param recordedProgramUri The URI of a recorded program. + */ + public void timeShiftPlay(String inputId, Uri recordedProgramUri) { + if (DEBUG) Log.d(TAG, "timeShiftPlay(" + recordedProgramUri + ")"); + if (TextUtils.isEmpty(inputId)) { + throw new IllegalArgumentException("inputId cannot be null or an empty string"); + } + synchronized (sMainTvViewLock) { + if (sMainTvView.get() == null) { + sMainTvView = new WeakReference<>(this); + } + } + if (mSessionCallback != null && TextUtils.equals(mSessionCallback.mInputId, inputId)) { + if (mSession != null) { + mSession.timeShiftPlay(recordedProgramUri); + } else { + mSessionCallback.mRecordedProgramUri = recordedProgramUri; + } + } else { + resetInternal(); + mSessionCallback = new MySessionCallback(inputId, recordedProgramUri); + if (mTvInputManager != null) { + mTvInputManager.createSession(inputId, mSessionCallback, mHandler); + } + } + } + + /** * Pauses playback. No-op if it is already paused. Call {@link #timeShiftResume} to resume. */ public void timeShiftPause() { @@ -994,6 +1025,7 @@ public class TvView extends ViewGroup { final String mInputId; Uri mChannelUri; Bundle mTuneParams; + Uri mRecordedProgramUri; MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) { mInputId = inputId; @@ -1001,6 +1033,11 @@ public class TvView extends ViewGroup { mTuneParams = tuneParams; } + MySessionCallback(String inputId, Uri recordedProgramUri) { + mInputId = inputId; + mRecordedProgramUri = recordedProgramUri; + } + @Override public void onSessionCreated(Session session) { if (DEBUG) { @@ -1043,7 +1080,11 @@ public class TvView extends ViewGroup { if (mCaptionEnabled != null) { mSession.setCaptionEnabled(mCaptionEnabled); } - mSession.tune(mChannelUri, mTuneParams); + if (mChannelUri != null) { + mSession.tune(mChannelUri, mTuneParams); + } else { + mSession.timeShiftPlay(mRecordedProgramUri); + } ensurePositionTracking(); } else { mSessionCallback = null; diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 3193ff8a4977..3ca99d400820 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -435,7 +435,11 @@ public final class TvInputManagerService extends SystemService { for (SessionState state : userState.sessionStateMap.values()) { if (state.session != null) { try { - state.session.release(); + if (state.isRecordingSession) { + state.session.disconnect(); + } else { + state.session.release(); + } } catch (RemoteException e) { Slog.e(TAG, "error in release", e); } @@ -604,7 +608,11 @@ public final class TvInputManagerService extends SystemService { // Create a session. When failed, send a null token immediately. try { - service.createSession(channels[1], callback, sessionState.info.getId()); + if (sessionState.isRecordingSession) { + service.createRecordingSession(callback, sessionState.info.getId()); + } else { + service.createSession(channels[1], callback, sessionState.info.getId()); + } } catch (RemoteException e) { Slog.e(TAG, "error in createSession", e); removeSessionStateLocked(sessionToken, userId); @@ -632,7 +640,11 @@ public final class TvInputManagerService extends SystemService { if (sessionToken == userState.mainSessionToken) { setMainLocked(sessionToken, false, callingUid, userId); } - sessionState.session.release(); + if (sessionState.isRecordingSession) { + sessionState.session.disconnect(); + } else { + sessionState.session.release(); + } } } catch (RemoteException | SessionNotFoundException e) { Slog.e(TAG, "error in releaseSession", e); @@ -766,6 +778,21 @@ public final class TvInputManagerService extends SystemService { } } + private void notifyTvInputInfoChanged(UserState userState, String inputId, + TvInputInfo inputInfo) { + if (DEBUG) { + Slog.d(TAG, "notifyTvInputInfoChanged(inputId=" + inputId + ", inputInfo=" + inputInfo + + ")"); + } + for (ITvInputManagerCallback callback : userState.callbackSet) { + try { + callback.onTvInputInfoChanged(inputId, inputInfo); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report changed input info to callback", e); + } + } + } + private void setStateLocked(String inputId, int state, int userId) { UserState userState = getOrCreateUserStateLocked(userId); TvInputState inputState = userState.inputMap.get(inputId); @@ -1005,7 +1032,7 @@ public final class TvInputManagerService extends SystemService { @Override public void createSession(final ITvInputClient client, final String inputId, - int seq, int userId) { + boolean isRecordingSession, int seq, int userId) { final int callingUid = Binder.getCallingUid(); final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, userId, "createSession"); @@ -1033,8 +1060,8 @@ public final class TvInputManagerService extends SystemService { // Create a new session token and a session state. IBinder sessionToken = new Binder(); - SessionState sessionState = new SessionState(sessionToken, info, client, - seq, callingUid, resolvedUserId); + SessionState sessionState = new SessionState(sessionToken, info, + isRecordingSession, client, seq, callingUid, resolvedUserId); // Add them to the global session state map of the current user. userState.sessionStateMap.put(sessionToken, sessionState); @@ -1375,6 +1402,26 @@ public final class TvInputManagerService extends SystemService { } @Override + public void timeShiftPlay(IBinder sessionToken, final Uri recordedProgramUri, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "timeShiftPlay"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).timeShiftPlay( + recordedProgramUri); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in timeShiftPlay", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void timeShiftPause(IBinder sessionToken, int userId) { final int callingUid = Binder.getCallingUid(); final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, @@ -1383,8 +1430,7 @@ public final class TvInputManagerService extends SystemService { try { synchronized (mLock) { try { - getSessionLocked(sessionToken, callingUid, resolvedUserId) - .timeShiftPause(); + getSessionLocked(sessionToken, callingUid, resolvedUserId).timeShiftPause(); } catch (RemoteException | SessionNotFoundException e) { Slog.e(TAG, "error in timeShiftPause", e); } @@ -1477,6 +1523,64 @@ public final class TvInputManagerService extends SystemService { } @Override + public void connect(IBinder sessionToken, final Uri channelUri, Bundle params, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "connect"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).connect( + channelUri, params); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in connect", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void startRecording(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "startRecording"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).startRecording(); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in startRecording", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void stopRecording(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "stopRecording"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getSessionLocked(sessionToken, callingUid, resolvedUserId).stopRecording(); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in stopRecording", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public List<TvInputHardwareInfo> getHardwareList() throws RemoteException { if (mContext.checkCallingPermission(android.Manifest.permission.TV_INPUT_HARDWARE) != PackageManager.PERMISSION_GRANTED) { @@ -1912,6 +2016,7 @@ public final class TvInputManagerService extends SystemService { private final class SessionState implements IBinder.DeathRecipient { private final TvInputInfo info; + private final boolean isRecordingSession; private final ITvInputClient client; private final int seq; private final int callingUid; @@ -1922,10 +2027,11 @@ public final class TvInputManagerService extends SystemService { // Not null if this session represents an external device connected to a hardware TV input. private IBinder hardwareSessionToken; - private SessionState(IBinder sessionToken, TvInputInfo info, ITvInputClient client, - int seq, int callingUid, int userId) { + private SessionState(IBinder sessionToken, TvInputInfo info, boolean isRecordingSession, + ITvInputClient client, int seq, int callingUid, int userId) { this.sessionToken = sessionToken; this.info = info; + this.isRecordingSession = isRecordingSession; this.client = client; this.seq = seq; this.callingUid = callingUid; @@ -2126,6 +2232,18 @@ public final class TvInputManagerService extends SystemService { } } } + + @Override + public void setTvInputInfo(String inputId, TvInputInfo inputInfo) { + ensureValidInput(inputInfo); + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "setTvInputInfo(" + inputInfo + ")"); + } + UserState userState = getOrCreateUserStateLocked(mUserId); + notifyTvInputInfoChanged(userState, inputId, inputInfo); + } + } } private final class SessionCallback extends ITvInputSessionCallback.Stub { @@ -2393,6 +2511,78 @@ public final class TvInputManagerService extends SystemService { } } } + + // For the recording session only + @Override + public void onConnected() { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onConnected()"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onConnected(mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onConnected", e); + } + } + } + + // For the recording session only + @Override + public void onRecordingStarted() { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onRecordingStarted()"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onRecordingStarted(mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onRecordingStarted", e); + } + } + } + + // For the recording session only + @Override + public void onRecordingStopped(Uri recordedProgramUri) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onRecordingStopped()"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onRecordingStopped(recordedProgramUri, mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onRecordingStopped", e); + } + } + } + + // For the recording session only + @Override + public void onError(int error) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onError()"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onError(error, mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onError", e); + } + } + } } private static final class WatchLogHandler extends Handler { |