TIF: Recording API
Bug: 23786643
Change-Id: Ie26e8944312048bc865b4506aa49fced15360fe4
diff --git a/api/current.txt b/api/current.txt
index b895dfe..035c76b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -22212,6 +22212,7 @@
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";
}
@@ -22348,13 +22349,49 @@
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);
@@ -22389,6 +22426,10 @@
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
@@ -22406,12 +22447,15 @@
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";
}
@@ -22424,6 +22468,18 @@
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);
@@ -22451,6 +22507,7 @@
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);
@@ -22461,6 +22518,23 @@
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();
@@ -22512,6 +22586,7 @@
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 f9f13fc..88fc3d7 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -23639,6 +23639,7 @@
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";
}
@@ -23779,6 +23780,40 @@
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";
@@ -23827,6 +23862,7 @@
}
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;
@@ -23838,6 +23874,7 @@
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();
@@ -23872,6 +23909,7 @@
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();
@@ -23895,6 +23933,10 @@
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
@@ -23959,16 +24001,19 @@
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";
}
@@ -23981,6 +24026,21 @@
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);
@@ -24011,6 +24071,7 @@
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);
@@ -24022,6 +24083,26 @@
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();
@@ -24104,6 +24185,7 @@
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 18e6a1c..4ff037b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -22220,6 +22220,7 @@
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";
}
@@ -22356,13 +22357,49 @@
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);
@@ -22397,6 +22434,10 @@
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
@@ -22414,12 +22455,15 @@
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";
}
@@ -22432,6 +22476,18 @@
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);
@@ -22459,6 +22515,7 @@
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);
@@ -22469,6 +22526,23 @@
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();
@@ -22520,6 +22594,7 @@
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 86c0e5d..8ef5ca0 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -43,4 +43,10 @@
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 f8057db..0febc16 100644
--- a/media/java/android/media/tv/ITvInputManager.aidl
+++ b/media/java/android/media/tv/ITvInputManager.aidl
@@ -55,7 +55,8 @@
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 @@
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 6792680..3bf415b 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 7a853d1..bd05184 100644
--- a/media/java/android/media/tv/ITvInputService.aidl
+++ b/media/java/android/media/tv/ITvInputService.aidl
@@ -27,10 +27,11 @@
* @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 74ab562..9f13882 100644
--- a/media/java/android/media/tv/ITvInputServiceCallback.aidl
+++ b/media/java/android/media/tv/ITvInputServiceCallback.aidl
@@ -27,4 +27,6 @@
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 6a06b8f..408a762 100644
--- a/media/java/android/media/tv/ITvInputSession.aidl
+++ b/media/java/android/media/tv/ITvInputSession.aidl
@@ -48,9 +48,16 @@
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 e936810..cb6a05e 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -40,4 +40,10 @@
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 f8c6f3f..4ac5876 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -59,20 +59,29 @@
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 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 @@
}
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 @@
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 @@
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 @@
}
@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 @@
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 7cd086e..62a01dc 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -54,6 +54,7 @@
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 @@
}
/**
+ * 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 @@
*
* <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 @@
}
/**
+ * 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 @@
/**
* 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 a3d748e..3960230 100644
--- a/media/java/android/media/tv/TvInputInfo.java
+++ b/media/java/android/media/tv/TvInputInfo.java
@@ -419,6 +419,27 @@
}
/**
+ * 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 6a13f82..f1de8fd 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 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 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 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 @@
}
});
}
+
+ // 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 @@
/**
* 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 @@
* <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 @@
* <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 @@
* <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,15 +748,6 @@
return mCallback;
}
- public void postInputStateChanged(final String inputId, final int state) {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mCallback.onInputStateChanged(inputId, state);
- }
- });
- }
-
public void postInputAdded(final String inputId) {
mHandler.post(new Runnable() {
@Override
@@ -669,6 +774,24 @@
}
});
}
+
+ public void postInputStateChanged(final String inputId, final int state) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ 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 @@
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 @@
}
}
}
+
+ @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 @@
* <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 @@
* <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 @@
@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 @@
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 @@
* 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 @@
/**
* 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 @@
* 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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 053d43b6..6d9b1ad 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 @@
}
@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 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 @@
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 @@
@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 @@
* @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 @@
// 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 @@
* @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 @@
* @see #notifyVideoUnavailable
*/
public void notifyVideoAvailable() {
- executeOrPostRunnable(new Runnable() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
@Override
public void run() {
try {
@@ -478,7 +525,8 @@
|| 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 @@
* @see TvInputManager
*/
public void notifyContentAllowed() {
- executeOrPostRunnable(new Runnable() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
@Override
public void run() {
try {
@@ -562,7 +611,8 @@
*/
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 @@
* </ul>
*/
public void notifyTimeShiftStatusChanged(final int status) {
- executeOrPostRunnable(new Runnable() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
@Override
public void run() {
try {
@@ -619,7 +670,8 @@
}
private void notifyTimeShiftStartPositionChanged(final long timeMs) {
- executeOrPostRunnable(new Runnable() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
@Override
public void run() {
try {
@@ -635,7 +687,8 @@
}
private void notifyTimeShiftCurrentPositionChanged(final long timeMs) {
- executeOrPostRunnable(new Runnable() {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
@Override
public void run() {
try {
@@ -665,7 +718,8 @@
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 @@
}
/**
+ * 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 @@
/**
* 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 @@
* 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 @@
* 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 @@
* 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 @@
* 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 @@
}
/**
+ * Calls {@link #onTimeShiftPlay(Uri)}.
+ */
+ void timeShiftPlay(Uri recordedProgramUri) {
+ mCurrentPositionMs = 0;
+ onTimeShiftPlay(recordedProgramUri);
+ }
+
+ /**
* Calls {@link #onTimeShiftPause}.
*/
void timeShiftPause() {
@@ -1385,7 +1467,7 @@
}
}
- 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 @@
}
/**
+ * 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 @@
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 @@
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 @@
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 @@
}
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 0000000..865e000
--- /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 003a274..0132d24 100644
--- a/media/java/android/media/tv/TvView.java
+++ b/media/java/android/media/tv/TvView.java
@@ -448,6 +448,37 @@
}
/**
+ * 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 @@
final String mInputId;
Uri mChannelUri;
Bundle mTuneParams;
+ Uri mRecordedProgramUri;
MySessionCallback(String inputId, Uri channelUri, Bundle tuneParams) {
mInputId = inputId;
@@ -1001,6 +1033,11 @@
mTuneParams = tuneParams;
}
+ MySessionCallback(String inputId, Uri recordedProgramUri) {
+ mInputId = inputId;
+ mRecordedProgramUri = recordedProgramUri;
+ }
+
@Override
public void onSessionCreated(Session session) {
if (DEBUG) {
@@ -1043,7 +1080,11 @@
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 3193ff8..3ca99d4 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 @@
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 @@
// 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 @@
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 @@
}
}
+ 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 @@
@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 @@
// 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 @@
}
@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 @@
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 @@
}
@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 @@
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 @@
// 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 @@
}
}
}
+
+ @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 @@
}
}
}
+
+ // 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 {