aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShraddha Basantwani <shraddha.basantwani@ittiam.com>2019-02-26 16:03:03 +0530
committerShraddha Basantwani <shraddha.basantwani@ittiam.com>2019-11-18 13:32:44 +0530
commitfc3f2d9eadeb434e4e77253ece65e152779e9589 (patch)
treea9b446823f68287accb253d3d0dd5ed0d48846e6
parent345d5fdf28c7316d522daa242e452b06473e5dac (diff)
downloadTV-fc3f2d9eadeb434e4e77253ece65e152779e9589.tar.gz
Start early and end late feature.
Programs can be recorded for extra time Added 5 new tests to DvrDbSyncTest Test: make RunTvRoboTests ROBOTEST_FILTER=DvrDbSyncTest Bug: 71718009 Change-Id: I4e3c8fc0acc9b56982000d2dbde2364d523a30e3
-rw-r--r--AndroidManifest.xml4
-rw-r--r--res/values/strings.xml23
-rw-r--r--src/com/android/tv/MainActivity.java12
-rw-r--r--src/com/android/tv/TvApplication.java6
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java17
-rw-r--r--src/com/android/tv/dvr/DvrManager.java25
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java146
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java14
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java115
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbFuture.java36
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java96
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java30
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java36
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordingSettingsActivity.java60
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordingSettingsFragment.java342
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java46
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java16
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java22
-rw-r--r--src/com/android/tv/modules/TvApplicationModule.java18
-rw-r--r--src/com/android/tv/ui/DetailsActivity.java3
-rw-r--r--src/com/android/tv/ui/ProgramDetailsFragment.java27
-rw-r--r--tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java154
23 files changed, 1155 insertions, 97 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7110160f..4c726c46 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -226,6 +226,10 @@
android:exported="true"
android:theme="@style/Theme.TV.Dvr.Browse.Details" />
<activity
+ android:name="com.android.tv.dvr.ui.DvrRecordingSettingsActivity"
+ android:configChanges="keyboard|keyboardHidden"
+ android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep" />
+ <activity
android:name="com.android.tv.dvr.ui.DvrSeriesSettingsActivity"
android:configChanges="keyboard|keyboardHidden"
android:theme="@style/Theme.TV.Dvr.Series.Settings.GuidedStep" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 06694a9d..e272244d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -717,6 +717,29 @@
<!-- Button label in priority settings fragment for one-time recordings -->
<string name="dvr_priority_action_one_time_recording">One-time recordings have the highest priority</string>
+ <!-- Title of DVR recording settings -->
+ <string name="dvr_recording_settings_title" translatable="false">Recording settings</string>
+ <!-- Title of recording start early settings fragment -->
+ <string name="dvr_start_early_title" translatable="false">Start early</string>
+ <!-- Title of recording end late settings fragment -->
+ <string name="dvr_end_late_title" translatable="false">End Late</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_none" translatable="false">On time</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_one_min" translatable="false">One Minute</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_five_mins" translatable="false">5 Minutes</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_fifteen_mins" translatable="false">15 Minutes</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_half_hour" translatable="false">30 Minutes</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_one_hour" translatable="false">60 Minutes</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_two_hours" translatable="false">120 Minutes</string>
+ <!-- Item description of time when the program has to be started early or ended late. -->
+ <string name="dvr_recording_settings_time_three_hours" translatable="false">180 Minutes</string>
+
<!-- DVR epg strings -->
<eat-comment />
<!-- Button text that deletes scheduled future recordings of a TV Program or Series. [CHAR LIMIT=50] -->
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 62fc0cee..75e51be3 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -110,6 +110,9 @@ import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.recorder.ConflictChecker;
+import com.android.tv.dvr.ui.DvrAlreadyRecordedFragment;
+import com.android.tv.dvr.ui.DvrAlreadyScheduledFragment;
+import com.android.tv.dvr.ui.DvrScheduleFragment;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.features.TvFeatures;
@@ -3017,5 +3020,14 @@ public class MainActivity extends Activity
@ContributesAndroidInjector
abstract ProgramItemView contributesProgramItemView();
+
+ @ContributesAndroidInjector
+ abstract DvrAlreadyRecordedFragment contributesDvrAlreadyRecordedFragment();
+
+ @ContributesAndroidInjector
+ abstract DvrAlreadyScheduledFragment contributesDvrAlreadyScheduledFragment();
+
+ @ContributesAndroidInjector
+ abstract DvrScheduleFragment contributesDvrScheduleFragment();
}
}
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index bc8226cf..663dcc3f 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -108,7 +108,7 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
@Inject Lazy<ChannelDataManager> mChannelDataManager;
private volatile ProgramDataManager mProgramDataManager;
private PreviewDataManager mPreviewDataManager;
- private DvrManager mDvrManager;
+ @Inject Lazy<DvrManager> mDvrManager;
private DvrScheduleManager mDvrScheduleManager;
@Inject Lazy<DvrDataManager> mDvrDataManager;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
@@ -213,7 +213,6 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
}
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrScheduleManager = new DvrScheduleManager(this);
- mDvrManager = new DvrManager(this);
mRecordingScheduler = RecordingScheduler.createScheduler(this);
}
mEpgFetcher.startRoutineService();
@@ -254,8 +253,9 @@ public abstract class TvApplication extends BaseApplication implements TvSinglet
/** Returns the {@link DvrManager}. */
@Override
+ @Nullable
public DvrManager getDvrManager() {
- return mDvrManager;
+ return (CommonFeatures.DVR.isEnabled(this)) ? mDvrManager.get() : null;
}
/** Returns the {@link DvrScheduleManager}. */
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 3e26a231..d759d65c 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -109,6 +109,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private final Context mContext;
private final DvrDatabaseHelper mDbHelper;
+ private final DvrDbSync.Factory mDvrDbSyncFactory;
+ private final DvrQueryScheduleFuture.Factory mDvrQueryScheduleFutureFactory;
private Executor mDbExecutor;
private final ContentObserver mContentObserver =
new ContentObserver(new Handler(Looper.getMainLooper())) {
@@ -204,7 +206,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
Clock clock,
TvInputManagerHelper tvInputManagerHelper,
@DbExecutor Executor dbExecutor,
- DvrDatabaseHelper dbHelper) {
+ DvrDatabaseHelper dbHelper,
+ DvrDbSync.Factory dvrDbSyncFactory,
+ DvrQueryScheduleFuture.Factory dvrQueryScheduleFutureFactory) {
super(context, clock);
mContext = context;
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
@@ -212,6 +216,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager();
mDbExecutor = dbExecutor;
mDbHelper = dbHelper;
+ mDvrQueryScheduleFutureFactory = dvrQueryScheduleFutureFactory;
+ mDvrDbSyncFactory = dvrDbSyncFactory;
start();
}
@@ -257,7 +263,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
});
mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
- DvrQueryScheduleFuture dvrQueryScheduleTask = new DvrQueryScheduleFuture(mDbHelper);
+ DvrQueryScheduleFuture dvrQueryScheduleTask =
+ mDvrQueryScheduleFutureFactory.create(mDbHelper);
ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
dvrQueryScheduleTask.executeOnDbThread(
new FutureCallback<List<ScheduledRecording>>() {
@@ -342,7 +349,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mDvrLoadFinished = true;
notifyDvrScheduleLoadFinished();
if (isInitialized()) {
- mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync = mDvrDbSyncFactory.create(
+ mContext,
+ DvrDataManagerImpl.this);
mDbSync.start();
SeriesRecordingScheduler.getInstance(mContext).start();
}
@@ -404,7 +413,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mRecordedProgramLoadFinished = true;
notifyRecordedProgramLoadFinished();
if (isInitialized()) {
- mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync = mDvrDbSyncFactory.create(mContext, DvrDataManagerImpl.this);
mDbSync.start();
}
} else if (recordedPrograms.isEmpty()) {
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 12982c6c..c6d1a29b 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -163,6 +163,18 @@ public class DvrManager {
/** Schedules a recording for {@code program}. */
public ScheduledRecording addSchedule(Program program) {
+ return addSchedule(program, 0, 0);
+ }
+
+ /**
+ * Schedules a recording for {@code program} with a early start time and late end time.
+ *
+ *@param startOffsetMs The extra time in milliseconds to start recording before the program
+ * starts.
+ *@param endOffsetMs The extra time in milliseconds to end recording after the program ends.
+ */
+ public ScheduledRecording addSchedule(Program program,
+ long startOffsetMs, long endOffsetMs) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
return null;
}
@@ -171,7 +183,9 @@ public class DvrManager {
program,
seriesRecording == null
? mScheduleManager.suggestNewPriority()
- : seriesRecording.getPriority());
+ : seriesRecording.getPriority(),
+ startOffsetMs,
+ endOffsetMs);
}
/**
@@ -192,10 +206,13 @@ public class DvrManager {
Range.create(
program.getStartTimeUtcMillis(),
program.getEndTimeUtcMillis()),
- seriesRecording.getPriority()));
+ seriesRecording.getPriority()),
+ 0,
+ 0);
}
- private ScheduledRecording addSchedule(Program program, long priority) {
+ private ScheduledRecording addSchedule(Program program, long priority,
+ long startOffsetMs, long endOffsetMs) {
TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program);
if (input == null) {
Log.e(TAG, "Can't find input for program: " + program);
@@ -210,6 +227,8 @@ public class DvrManager {
seriesRecording == null
? SeriesRecording.ID_NOT_SET
: seriesRecording.getId())
+ .setStartOffsetMs(startOffsetMs)
+ .setEndOffsetMs(endOffsetMs)
.build();
mDataManager.addScheduledRecording(schedule);
return schedule;
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 1237fb3e..9e89ab62 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -56,6 +56,9 @@ public final class ScheduledRecording implements Parcelable {
/** The default priority of the recording. */
public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
+ /** The default offset time of the recording. */
+ public static final long DEFAULT_TIME_OFFSET = 0;
+
/** Compares the start time in ascending order. */
public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR =
(ScheduledRecording lhs, ScheduledRecording rhs) ->
@@ -155,6 +158,8 @@ public final class ScheduledRecording implements Parcelable {
private long mSeriesRecordingId = ID_NOT_SET;
private Long mRecodedProgramId;
private Integer mFailedReason;
+ private long mStartOffsetMs = DEFAULT_TIME_OFFSET;
+ private long mEndOffsetMs = DEFAULT_TIME_OFFSET;
private Builder() {}
@@ -258,6 +263,16 @@ public final class ScheduledRecording implements Parcelable {
return this;
}
+ public Builder setStartOffsetMs(long startOffsetMs) {
+ mStartOffsetMs = Math.max(0, startOffsetMs);
+ return this;
+ }
+
+ public Builder setEndOffsetMs(long endOffsetMs) {
+ mEndOffsetMs = Math.max(0, endOffsetMs);
+ return this;
+ }
+
public ScheduledRecording build() {
return new ScheduledRecording(
mId,
@@ -279,7 +294,9 @@ public final class ScheduledRecording implements Parcelable {
mState,
mSeriesRecordingId,
mRecodedProgramId,
- mFailedReason);
+ mFailedReason,
+ mStartOffsetMs,
+ mEndOffsetMs);
}
}
@@ -304,7 +321,9 @@ public final class ScheduledRecording implements Parcelable {
.setProgramThumbnailUri(orig.getProgramThumbnailUri())
.setState(orig.mState)
.setFailedReason(orig.getFailedReason())
- .setType(orig.mType);
+ .setType(orig.mType)
+ .setStartOffsetMs(orig.getStartOffsetMs())
+ .setEndOffsetMs(orig.getEndOffsetMs());
}
@Retention(RetentionPolicy.SOURCE)
@@ -401,6 +420,35 @@ public final class ScheduledRecording implements Parcelable {
Schedules.COLUMN_SERIES_RECORDING_ID
};
+ /**
+ * Use this projection if you want to create {@link ScheduledRecording} object using {@link
+ * #fromCursorWithTimeOffset}.
+ */
+ public static final String[] PROJECTION_WITH_TIME_OFFSET = {
+ // Columns must match what is read in #fromCursor
+ Schedules._ID,
+ Schedules.COLUMN_PRIORITY,
+ Schedules.COLUMN_TYPE,
+ Schedules.COLUMN_INPUT_ID,
+ Schedules.COLUMN_CHANNEL_ID,
+ Schedules.COLUMN_PROGRAM_ID,
+ Schedules.COLUMN_PROGRAM_TITLE,
+ Schedules.COLUMN_START_TIME_UTC_MILLIS,
+ Schedules.COLUMN_END_TIME_UTC_MILLIS,
+ Schedules.COLUMN_SEASON_NUMBER,
+ Schedules.COLUMN_EPISODE_NUMBER,
+ Schedules.COLUMN_EPISODE_TITLE,
+ Schedules.COLUMN_PROGRAM_DESCRIPTION,
+ Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION,
+ Schedules.COLUMN_PROGRAM_POST_ART_URI,
+ Schedules.COLUMN_PROGRAM_THUMBNAIL_URI,
+ Schedules.COLUMN_STATE,
+ Schedules.COLUMN_FAILED_REASON,
+ Schedules.COLUMN_SERIES_RECORDING_ID,
+ Schedules.COLUMN_START_OFFSET_MILLIS,
+ Schedules.COLUMN_END_OFFSET_MILLIS
+ };
+
/** Creates {@link ScheduledRecording} object from the given {@link Cursor}. */
public static ScheduledRecording fromCursor(Cursor c) {
int index = -1;
@@ -427,6 +475,34 @@ public final class ScheduledRecording implements Parcelable {
.build();
}
+ /** Creates {@link ScheduledRecording} object from the given {@link Cursor}. */
+ public static ScheduledRecording fromCursorWithTimeOffset(Cursor c) {
+ int index = -1;
+ return new Builder()
+ .setId(c.getLong(++index))
+ .setPriority(c.getLong(++index))
+ .setType(recordingType(c.getString(++index)))
+ .setInputId(c.getString(++index))
+ .setChannelId(c.getLong(++index))
+ .setProgramId(c.getLong(++index))
+ .setProgramTitle(c.getString(++index))
+ .setStartTimeMs(c.getLong(++index))
+ .setEndTimeMs(c.getLong(++index))
+ .setSeasonNumber(c.getString(++index))
+ .setEpisodeNumber(c.getString(++index))
+ .setEpisodeTitle(c.getString(++index))
+ .setProgramDescription(c.getString(++index))
+ .setProgramLongDescription(c.getString(++index))
+ .setProgramPosterArtUri(c.getString(++index))
+ .setProgramThumbnailUri(c.getString(++index))
+ .setState(recordingState(c.getString(++index)))
+ .setFailedReason(recordingFailedReason(c.getString(++index)))
+ .setSeriesRecordingId(c.getLong(++index))
+ .setStartOffsetMs(c.getLong(++index))
+ .setEndOffsetMs(c.getLong(++index))
+ .build();
+ }
+
public static ContentValues toContentValues(ScheduledRecording r) {
ContentValues values = new ContentValues();
if (r.getId() != ID_NOT_SET) {
@@ -457,6 +533,38 @@ public final class ScheduledRecording implements Parcelable {
return values;
}
+ public static ContentValues toContentValuesWithTimeOffset(ScheduledRecording r) {
+ ContentValues values = new ContentValues();
+ if (r.getId() != ID_NOT_SET) {
+ values.put(Schedules._ID, r.getId());
+ }
+ values.put(Schedules.COLUMN_INPUT_ID, r.getInputId());
+ values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId());
+ values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId());
+ values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle());
+ values.put(Schedules.COLUMN_PRIORITY, r.getPriority());
+ values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
+ values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
+ values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber());
+ values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber());
+ values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle());
+ values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription());
+ values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription());
+ values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri());
+ values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri());
+ values.put(Schedules.COLUMN_STATE, recordingState(r.getState()));
+ values.put(Schedules.COLUMN_FAILED_REASON, recordingFailedReason(r.getFailedReason()));
+ values.put(Schedules.COLUMN_TYPE, recordingType(r.getType()));
+ if (r.getSeriesRecordingId() != ID_NOT_SET) {
+ values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId());
+ } else {
+ values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID);
+ }
+ values.put(Schedules.COLUMN_START_OFFSET_MILLIS, r.getStartOffsetMs());
+ values.put(Schedules.COLUMN_END_OFFSET_MILLIS, r.getEndOffsetMs());
+ return values;
+ }
+
public static ScheduledRecording fromParcel(Parcel in) {
return new Builder()
.setId(in.readLong())
@@ -478,6 +586,8 @@ public final class ScheduledRecording implements Parcelable {
.setState(in.readInt())
.setFailedReason(recordingFailedReason(in.readString()))
.setSeriesRecordingId(in.readLong())
+ .setStartOffsetMs(in.readLong())
+ .setEndOffsetMs(in.readLong())
.build();
}
@@ -525,6 +635,8 @@ public final class ScheduledRecording implements Parcelable {
private final long mSeriesRecordingId;
private final Long mRecordedProgramId;
private final Integer mFailedReason;
+ private final long mStartOffsetMs;
+ private final long mEndOffsetMs;
private ScheduledRecording(
long id,
@@ -546,7 +658,9 @@ public final class ScheduledRecording implements Parcelable {
@RecordingState int state,
long seriesRecordingId,
Long recordedProgramId,
- Integer failedReason) {
+ Integer failedReason,
+ long startOffsetMs,
+ long endOffsetMs) {
mId = id;
mPriority = priority;
mInputId = inputId;
@@ -567,6 +681,8 @@ public final class ScheduledRecording implements Parcelable {
mSeriesRecordingId = seriesRecordingId;
mRecordedProgramId = recordedProgramId;
mFailedReason = failedReason;
+ mStartOffsetMs = startOffsetMs;
+ mEndOffsetMs = endOffsetMs;
}
/**
@@ -677,6 +793,16 @@ public final class ScheduledRecording implements Parcelable {
return mFailedReason;
}
+ /** Returns the start time offset. */
+ public long getStartOffsetMs() {
+ return mStartOffsetMs;
+ }
+
+ /** Returns the end time offset. */
+ public long getEndOffsetMs() {
+ return mEndOffsetMs;
+ }
+
public long getId() {
return mId;
}
@@ -929,6 +1055,10 @@ public final class ScheduledRecording implements Parcelable {
+ mPriority
+ ",seriesRecordingId="
+ mSeriesRecordingId
+ + ",startOffsetMs="
+ + mStartOffsetMs
+ + ",endOffsetMs="
+ + mEndOffsetMs
+ ")";
}
@@ -958,6 +1088,8 @@ public final class ScheduledRecording implements Parcelable {
out.writeInt(mState);
out.writeString(recordingFailedReason(mFailedReason));
out.writeLong(mSeriesRecordingId);
+ out.writeLong(mStartOffsetMs);
+ out.writeLong(mEndOffsetMs);
}
/** Returns {@code true} if the recording is not started yet, otherwise @{code false}. */
@@ -1003,7 +1135,9 @@ public final class ScheduledRecording implements Parcelable {
&& Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri())
&& mState == r.mState
&& Objects.equals(mFailedReason, r.mFailedReason)
- && mSeriesRecordingId == r.mSeriesRecordingId;
+ && mSeriesRecordingId == r.mSeriesRecordingId
+ && mStartOffsetMs == r.mStartOffsetMs
+ && mEndOffsetMs == r.mEndOffsetMs;
}
@Override
@@ -1026,7 +1160,9 @@ public final class ScheduledRecording implements Parcelable {
mProgramThumbnailUri,
mState,
mFailedReason,
- mSeriesRecordingId);
+ mSeriesRecordingId,
+ mStartOffsetMs,
+ mEndOffsetMs);
}
/** Returns an array containing all of the elements in the list. */
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index 8539ae36..1ac477e8 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -253,6 +253,20 @@ public final class DvrContract {
*/
public static final String COLUMN_SERIES_RECORDING_ID = "series_recording_id";
+ /**
+ * The extra time in milliseconds to start recording before the program starts.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_START_OFFSET_MILLIS = "start_offset_millis";
+
+ /**
+ * The extra time in milliseconds to end recording after the program ends.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_END_OFFSET_MILLIS = "end_offset_millis";
+
private Schedules() {}
}
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index fa48791d..51fa541c 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -28,11 +28,14 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.dagger.annotations.ApplicationContext;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
+import com.google.common.collect.ObjectArrays;
+
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -89,12 +92,35 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
referenceConstraint(SeriesRecordings.TABLE_NAME, SeriesRecordings._ID))
};
+ private static final ColumnInfo[] COLUMNS_TIME_OFFSET =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ Schedules.COLUMN_START_OFFSET_MILLIS,
+ SQL_DATA_TYPE_LONG,
+ defaultConstraint(ScheduledRecording.DEFAULT_TIME_OFFSET)),
+ new ColumnInfo(
+ Schedules.COLUMN_END_OFFSET_MILLIS,
+ SQL_DATA_TYPE_LONG,
+ defaultConstraint(ScheduledRecording.DEFAULT_TIME_OFFSET))
+ };
+
+ private static final ColumnInfo[] COLUMNS_SCHEDULES_WITH_TIME_OFFSET =
+ ObjectArrays.concat(COLUMNS_SCHEDULES, COLUMNS_TIME_OFFSET, ColumnInfo.class);
+
private static final String SQL_CREATE_SCHEDULES =
buildCreateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
private static final String SQL_INSERT_SCHEDULES =
buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
private static final String SQL_UPDATE_SCHEDULES =
buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
+
+ private static final String SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET =
+ buildCreateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
+ private static final String SQL_INSERT_SCHEDULES_WITH_TIME_OFFSET =
+ buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
+ private static final String SQL_UPDATE_SCHEDULES_WITH_TIME_OFFSET =
+ buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
+
private static final String SQL_DELETE_SCHEDULES = buildDeleteSql(Schedules.TABLE_NAME);
private static final String SQL_DROP_SCHEDULES = buildDropSql(Schedules.TABLE_NAME);
@@ -140,6 +166,8 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String SQL_DROP_SERIES_RECORDINGS =
buildDropSql(SeriesRecordings.TABLE_NAME);
+ private final DvrFlags mDvrFlags;
+
private static String defaultConstraint(int value) {
return defaultConstraint(String.valueOf(value));
}
@@ -243,8 +271,12 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
}
@Inject
- public DvrDatabaseHelper(@ApplicationContext Context context) {
- super(context, DB_NAME, null, DATABASE_VERSION);
+ public DvrDatabaseHelper(@ApplicationContext Context context, DvrFlags dvrFlags) {
+ super(context,
+ DB_NAME,
+ null,
+ (dvrFlags.startEarlyEndLateEnabled() ? DATABASE_VERSION + 1 : DATABASE_VERSION));
+ mDvrFlags = dvrFlags;
}
@Override
@@ -254,8 +286,13 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
- if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES);
- db.execSQL(SQL_CREATE_SCHEDULES);
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET);
+ db.execSQL(SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET);
+ } else {
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES);
+ db.execSQL(SQL_CREATE_SCHEDULES);
+ }
if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SERIES_RECORDINGS);
db.execSQL(SQL_CREATE_SERIES_RECORDINGS);
}
@@ -278,6 +315,27 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
+ Schedules.COLUMN_FAILED_REASON
+ " TEXT DEFAULT null;");
}
+ if (mDvrFlags.startEarlyEndLateEnabled() && oldVersion < 19) {
+ db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
+ + Schedules.COLUMN_START_OFFSET_MILLIS + " INTEGER NOT NULL DEFAULT '0';");
+ db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
+ + Schedules.COLUMN_END_OFFSET_MILLIS + " INTEGER NOT NULL DEFAULT '0';");
+ }
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion > DATABASE_VERSION) {
+ String schedulesBackup = "schedules_backup";
+ db.execSQL(buildCreateSql(schedulesBackup, COLUMNS_SCHEDULES));
+ db.execSQL("INSERT INTO " + schedulesBackup +
+ buildSelectSql(COLUMNS_SCHEDULES) + " FROM " + Schedules.TABLE_NAME);
+ db.execSQL(SQL_DROP_SCHEDULES);
+ db.execSQL(SQL_CREATE_SCHEDULES);
+ db.execSQL("INSERT INTO " + Schedules.TABLE_NAME +
+ buildSelectSql(COLUMNS_SCHEDULES) + " FROM " + schedulesBackup);
+ db.execSQL(buildDropSql(schedulesBackup));
+ }
}
/** Handles the query request and returns a {@link Cursor}. */
@@ -291,14 +349,25 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
/** Inserts schedules. */
public void insertSchedules(ScheduledRecording... scheduledRecordings) {
SQLiteDatabase db = getWritableDatabase();
- SQLiteStatement statement = db.compileStatement(SQL_INSERT_SCHEDULES);
db.beginTransaction();
try {
- for (ScheduledRecording r : scheduledRecordings) {
- statement.clearBindings();
- ContentValues values = ScheduledRecording.toContentValues(r);
- bindColumns(statement, COLUMNS_SCHEDULES, values);
- statement.execute();
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ SQLiteStatement statement =
+ db.compileStatement(SQL_INSERT_SCHEDULES_WITH_TIME_OFFSET);
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValuesWithTimeOffset(r);
+ bindColumns(statement, COLUMNS_SCHEDULES_WITH_TIME_OFFSET, values);
+ statement.execute();
+ }
+ } else {
+ SQLiteStatement statement = db.compileStatement(SQL_INSERT_SCHEDULES);
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SCHEDULES, values);
+ statement.execute();
+ }
}
db.setTransactionSuccessful();
} finally {
@@ -309,15 +378,27 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
/** Update schedules. */
public void updateSchedules(ScheduledRecording... scheduledRecordings) {
SQLiteDatabase db = getWritableDatabase();
- SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SCHEDULES);
db.beginTransaction();
try {
- for (ScheduledRecording r : scheduledRecordings) {
- statement.clearBindings();
- ContentValues values = ScheduledRecording.toContentValues(r);
- bindColumns(statement, COLUMNS_SCHEDULES, values);
- statement.bindLong(COLUMNS_SCHEDULES.length + 1, r.getId());
- statement.execute();
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ SQLiteStatement statement =
+ db.compileStatement(SQL_UPDATE_SCHEDULES_WITH_TIME_OFFSET);
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValuesWithTimeOffset(r);
+ bindColumns(statement, COLUMNS_SCHEDULES_WITH_TIME_OFFSET, values);
+ statement.bindLong(COLUMNS_SCHEDULES_WITH_TIME_OFFSET.length + 1, r.getId());
+ statement.execute();
+ }
+ } else {
+ SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SCHEDULES);
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SCHEDULES, values);
+ statement.bindLong(COLUMNS_SCHEDULES.length + 1, r.getId());
+ statement.execute();
+ }
}
db.setTransactionSuccessful();
} finally {
diff --git a/src/com/android/tv/dvr/provider/DvrDbFuture.java b/src/com/android/tv/dvr/provider/DvrDbFuture.java
index cbc2c07d..d6ac1c4c 100644
--- a/src/com/android/tv/dvr/provider/DvrDbFuture.java
+++ b/src/com/android/tv/dvr/provider/DvrDbFuture.java
@@ -21,12 +21,15 @@ import android.support.annotation.Nullable;
import android.util.Log;
import com.android.tv.common.concurrent.NamedThreadFactory;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.MainThreadExecutor;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -109,8 +112,23 @@ public abstract class DvrDbFuture<ParamsT, ResultT> {
/** Returns all {@link ScheduledRecording}s. */
public static class DvrQueryScheduleFuture extends DvrDbFuture<Void, List<ScheduledRecording>> {
- public DvrQueryScheduleFuture(DvrDatabaseHelper dbHelper) {
+
+ private final DvrFlags mDvrFlags;
+
+ /**
+ * Factory for {@link DvrQueryScheduleFuture}.
+ *
+ * <p>This wrapper class keeps other classes from needing to reference the
+ * {@link AutoFactory} generated class.
+ */
+ public interface Factory {
+ public DvrQueryScheduleFuture create(DvrDatabaseHelper dbHelper);
+ }
+
+ @AutoFactory(implementing = Factory.class, className = "DvrQueryScheduleFutureFactory")
+ public DvrQueryScheduleFuture(DvrDatabaseHelper dbHelper, @Provided DvrFlags dvrFlags) {
super(dbHelper);
+ mDvrFlags = dvrFlags;
}
@Override
@@ -120,9 +138,19 @@ public abstract class DvrDbFuture<ParamsT, ResultT> {
return null;
}
List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
- try (Cursor c = mDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
- while (c.moveToNext() && !isCancelled()) {
- scheduledRecordings.add(ScheduledRecording.fromCursor(c));
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ try (Cursor c = mDbHelper.query(Schedules.TABLE_NAME,
+ ScheduledRecording.PROJECTION_WITH_TIME_OFFSET)) {
+ while (c.moveToNext() && !isCancelled()) {
+ scheduledRecordings.add(ScheduledRecording.fromCursorWithTimeOffset(c));
+ }
+ }
+ } else {
+ try (Cursor c = mDbHelper.query(Schedules.TABLE_NAME,
+ ScheduledRecording.PROJECTION)) {
+ while (c.moveToNext() && !isCancelled()) {
+ scheduledRecordings.add(ScheduledRecording.fromCursor(c));
+ }
}
}
return scheduledRecordings;
diff --git a/src/com/android/tv/dvr/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index c2eae771..88d9cc46 100644
--- a/src/com/android/tv/dvr/provider/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -30,7 +30,7 @@ import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
-import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
@@ -40,7 +40,10 @@ import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
+import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.TvUriMatcher;
+import com.google.auto.factory.AutoFactory;
+import com.google.auto.factory.Provided;
import java.util.ArrayList;
import java.util.Collections;
@@ -76,6 +79,7 @@ public class DvrDbSync {
private final ChannelDataManager mChannelDataManager;
private final Executor mDbExecutor;
private final Queue<Long> mProgramIdQueue = new LinkedList<>();
+ private final DvrFlags mDvrFlags;
private QueryProgramTask mQueryProgramTask;
private final SeriesRecordingScheduler mSeriesRecordingScheduler;
private final ContentObserver mContentObserver =
@@ -143,26 +147,46 @@ public class DvrDbSync {
}
};
- public DvrDbSync(Context context, WritableDvrDataManager dataManager) {
+ /**
+ * Factory for {@link DvrDbSync}.
+ *
+ * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
+ * generated class.
+ */
+ public interface Factory {
+ public DvrDbSync create(Context context, WritableDvrDataManager dataManager);
+ }
+
+ @AutoFactory(implementing = Factory.class)
+ public DvrDbSync(
+ Context context,
+ WritableDvrDataManager dataManager,
+ @Provided DvrFlags dvrFlags,
+ @Provided ChannelDataManager channelDataManager,
+ @Provided DvrManager dvrManager,
+ @Provided @DbExecutor Executor dbExecutor) {
this(
context,
dataManager,
- TvSingletons.getSingletons(context).getChannelDataManager(),
- TvSingletons.getSingletons(context).getDvrManager(),
+ dvrFlags,
+ channelDataManager,
+ dvrManager,
SeriesRecordingScheduler.getInstance(context),
- TvSingletons.getSingletons(context).getDbExecutor());
+ dbExecutor);
}
@VisibleForTesting
DvrDbSync(
Context context,
WritableDvrDataManager dataManager,
+ DvrFlags dvrFlags,
ChannelDataManager channelDataManager,
DvrManager dvrManager,
SeriesRecordingScheduler seriesRecordingScheduler,
Executor dbExecutor) {
mContext = context;
mDvrManager = dvrManager;
+ mDvrFlags = dvrFlags;
mDataManager = dataManager;
mChannelDataManager = channelDataManager;
mSeriesRecordingScheduler = seriesRecordingScheduler;
@@ -284,7 +308,6 @@ public class DvrDbSync {
} else {
ScheduledRecording.Builder builder =
ScheduledRecording.buildFrom(schedule)
- .setEndTimeMs(program.getEndTimeUtcMillis())
.setSeasonNumber(program.getSeasonNumber())
.setEpisodeNumber(program.getEpisodeNumber())
.setEpisodeTitle(program.getEpisodeTitle())
@@ -333,18 +356,30 @@ public class DvrDbSync {
// Change start time only when the recording is not started yet and if it is not
// within marginal time of current time. Marginal check is needed to prevent the
// update of start time if recording is just triggered or about to get triggered.
- boolean marginalToCurrentTime = RECORD_MARGIN_MS >
- Math.abs(System.currentTimeMillis() - schedule.getStartTimeMs());
- boolean needToChangeStartTime =
- schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
- && program.getStartTimeUtcMillis() != schedule.getStartTimeMs()
- && !marginalToCurrentTime;
- if (needToChangeStartTime) {
- builder.setStartTimeMs(program.getStartTimeUtcMillis());
- needUpdate = true;
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ ScheduledRecording.Builder builderUpdatedTime =
+ handleUpdateProgramTime(program, schedule, builder);
+ if (builderUpdatedTime != null) {
+ builder = builderUpdatedTime;
+ needUpdate = true;
+ }
+ } else {
+ boolean marginalToCurrentTime = RECORD_MARGIN_MS >
+ Math.abs(System.currentTimeMillis() - schedule.getStartTimeMs());
+ boolean needToChangeStartTime =
+ schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ && program.getStartTimeUtcMillis() != schedule.getStartTimeMs()
+ && !marginalToCurrentTime;
+ if (needToChangeStartTime) {
+ builder.setStartTimeMs(program.getStartTimeUtcMillis());
+ needUpdate = true;
+ }
+ if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis()) {
+ builder.setEndTimeMs(program.getEndTimeUtcMillis());
+ needUpdate = true;
+ }
}
if (needUpdate
- || schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
|| !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
|| !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
|| !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
@@ -366,6 +401,35 @@ public class DvrDbSync {
}
}
+ private static ScheduledRecording.Builder handleUpdateProgramTime(
+ Program program, ScheduledRecording schedule, ScheduledRecording.Builder builder) {
+ boolean needUpdate = false;
+ long currentTime = System.currentTimeMillis();
+ boolean marginalToCurrentTime = RECORD_MARGIN_MS >
+ Math.abs(currentTime - schedule.getStartTimeMs());
+ long updatedStartTime = program.getStartTimeUtcMillis() - schedule.getStartOffsetMs();
+ boolean needToChangeStartTime =
+ schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ && schedule.getStartTimeMs() != updatedStartTime
+ && !marginalToCurrentTime;
+ if (needToChangeStartTime) {
+ // Check if updated program time has already passed.
+ if (updatedStartTime < currentTime) {
+ updatedStartTime = currentTime + RECORD_MARGIN_MS;
+ long updatedStartOffset = program.getStartTimeUtcMillis() - updatedStartTime;
+ builder.setStartOffsetMs(updatedStartOffset > 0 ? updatedStartOffset : 0);
+ }
+ builder.setStartTimeMs(updatedStartTime);
+ needUpdate = true;
+ }
+ long updatedEndTime = program.getEndTimeUtcMillis() + schedule.getEndOffsetMs();
+ if (schedule.getEndTimeMs() != updatedEndTime) {
+ builder.setEndTimeMs(updatedEndTime);
+ needUpdate = true;
+ }
+ return (needUpdate ? builder : null);
+ }
+
private class QueryProgramTask extends AsyncQueryProgramTask {
private final long mProgramId;
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index 1f4faf31..028265b2 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -28,12 +28,16 @@ import androidx.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.RecordedProgram;
import java.util.List;
+import javax.inject.Inject;
+import dagger.android.AndroidInjection;
+
/**
* A fragment which notifies the user that the same episode has already been scheduled.
*
@@ -47,9 +51,11 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
private Program mProgram;
private RecordedProgram mDuplicate;
+ @Inject DvrFlags mDvrFlags;
@Override
public void onAttach(Context context) {
+ AndroidInjection.inject(this);
super.onAttach(context);
mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager();
@@ -59,13 +65,17 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
mProgram.getSeasonNumber(),
mProgram.getEpisodeNumber());
if (mDuplicate == null) {
- dvrManager.addSchedule(mProgram);
- DvrUiHelper.showAddScheduleToast(
- context,
- mProgram.getTitle(),
- mProgram.getStartTimeUtcMillis(),
- mProgram.getEndTimeUtcMillis());
- dismissDialog();
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ dvrManager.addSchedule(mProgram);
+ DvrUiHelper.showAddScheduleToast(
+ context,
+ mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(),
+ mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ }
}
}
@@ -101,7 +111,11 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_ANYWAY) {
- getDvrManager().addSchedule(mProgram);
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ getDvrManager().addSchedule(mProgram);
+ }
} else if (action.getId() == ACTION_WATCH) {
DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null, false);
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 56ffc884..4a3a5d4d 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -29,12 +29,16 @@ import androidx.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import java.util.List;
+import javax.inject.Inject;
+import dagger.android.AndroidInjection;
+
/**
* A fragment which notifies the user that the same episode has already been scheduled.
*
@@ -48,9 +52,11 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
private Program mProgram;
private ScheduledRecording mDuplicate;
+ @Inject DvrFlags mDvrFlags;
@Override
public void onAttach(Context context) {
+ AndroidInjection.inject(this);
super.onAttach(context);
mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
DvrManager dvrManager = TvSingletons.getSingletons(context).getDvrManager();
@@ -60,13 +66,17 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
mProgram.getSeasonNumber(),
mProgram.getEpisodeNumber());
if (mDuplicate == null) {
- dvrManager.addSchedule(mProgram);
- DvrUiHelper.showAddScheduleToast(
- context,
- mProgram.getTitle(),
- mProgram.getStartTimeUtcMillis(),
- mProgram.getEndTimeUtcMillis());
- dismissDialog();
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ dvrManager.addSchedule(mProgram);
+ DvrUiHelper.showAddScheduleToast(
+ context,
+ mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(),
+ mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ }
}
}
@@ -108,10 +118,18 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_ANYWAY) {
- getDvrManager().addSchedule(mProgram);
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ getDvrManager().addSchedule(mProgram);
+ }
} else if (action.getId() == ACTION_RECORD_INSTEAD) {
- getDvrManager().addSchedule(mProgram);
getDvrManager().removeScheduledRecording(mDuplicate);
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ getDvrManager().addSchedule(mProgram);
+ }
}
dismissDialog();
}
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index 5e0a96bb..47a436da 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -92,6 +92,10 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
getContext(), getConflicts());
}
dismissDialog();
+ // Finish the Recording setting Activity on dismissal.
+ if (getActivity() instanceof DvrRecordingSettingsActivity) {
+ getActivity().finish();
+ }
}
@Override
diff --git a/src/com/android/tv/dvr/ui/DvrRecordingSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrRecordingSettingsActivity.java
new file mode 100644
index 00000000..9cf0bad2
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrRecordingSettingsActivity.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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 com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Bundle;
+import androidx.leanback.app.GuidedStepFragment;
+import com.android.tv.R;
+import com.android.tv.Starter;
+
+/** Activity to show details view in DVR. */
+public class DvrRecordingSettingsActivity extends Activity {
+
+ /**
+ * Name of the boolean flag to decide if the setting fragment should be translucent. Type:
+ * boolean
+ */
+ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent";
+ /**
+ * Name of the program added for recording.
+ */
+ public static final String PROGRAM = "program";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Starter.start(this);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_series_settings);
+
+ if (savedInstanceState == null) {
+ DvrRecordingSettingsFragment settingFragment = new DvrRecordingSettingsFragment();
+ settingFragment.setArguments(getIntent().getExtras());
+ GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ if (!getIntent().getExtras().getBoolean(IS_WINDOW_TRANSLUCENT, true)) {
+ getWindow()
+ .setBackgroundDrawable(
+ new ColorDrawable(getColor(R.color.common_tv_background)));
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordingSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrRecordingSettingsFragment.java
new file mode 100644
index 00000000..8e651b81
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrRecordingSettingsFragment.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2019 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 com.android.tv.dvr.ui;
+
+import android.annotation.TargetApi;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.TvSingletons;
+import com.android.tv.data.ProgramImpl;
+import com.android.tv.data.api.Program;
+import com.android.tv.dialog.SafeDismissDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import androidx.leanback.app.GuidedStepFragment;
+import androidx.leanback.widget.GuidanceStylist.Guidance;
+import androidx.leanback.widget.GuidedAction;
+import androidx.leanback.widget.GuidedActionsStylist;
+
+/** Fragment for DVR recording settings. */
+@TargetApi(Build.VERSION_CODES.N)
+@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
+public class DvrRecordingSettingsFragment extends GuidedStepFragment {
+ private static final String TAG = "RecordingSettingsFragment";
+
+ private static final long ACTION_ID_START_EARLY = 100;
+ private static final long ACTION_ID_END_LATE = 101;
+
+ private static final int SUB_ACTION_ID_START_ON_TIME = 1;
+ private static final int SUB_ACTION_ID_START_ONE_MIN = 2;
+ private static final int SUB_ACTION_ID_START_FIVE_MIN = 3;
+ private static final int SUB_ACTION_ID_START_FIFTEEN_MIN = 4;
+ private static final int SUB_ACTION_ID_START_HALF_HOUR = 5;
+
+ private static final int SUB_ACTION_ID_END_ON_TIME = 6;
+ private static final int SUB_ACTION_ID_END_ONE_MIN = 7;
+ private static final int SUB_ACTION_ID_END_FIFTEEN_MIN = 8;
+ private static final int SUB_ACTION_ID_END_HALF_HOUR = 9;
+ private static final int SUB_ACTION_ID_END_ONE_HOUR = 10;
+ private static final int SUB_ACTION_ID_END_TWO_HOURS = 11;
+ private static final int SUB_ACTION_ID_END_THREE_HOURS = 12;
+
+ private Program mProgram;
+ private String mFragmentTitle;
+ private String mStartEarlyActionTitle;
+ private String mEndLateActionTitle;
+ private String mTimeActionOnTimeText;
+ private String mTimeActionOneMinText;
+ private String mTimeActionFiveMinText;
+ private String mTimeActionFifteenMinText;
+ private String mTimeActionHalfHourText;
+ private String mTimeActionOneHourText;
+ private String mTimeActionTwoHoursText;
+ private String mTimeActionThreeHoursText;
+
+ private GuidedAction mStartEarlyGuidedAction;
+ private GuidedAction mEndLateGuidedAction;
+ private long mStartEarlyTime = 0;
+ private long mEndLateTime = 0;
+ private DvrManager mDvrManager;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
+ mProgram = getArguments().getParcelable(DvrRecordingSettingsActivity.PROGRAM);
+ if (mProgram == null) {
+ getActivity().finish();
+ return;
+ }
+ mFragmentTitle = getString(R.string.dvr_recording_settings_title);
+ mStartEarlyActionTitle = getString(R.string.dvr_start_early_title);
+ mEndLateActionTitle = getString(R.string.dvr_end_late_title);
+ mTimeActionOnTimeText = getString(R.string.dvr_recording_settings_time_none);
+ mTimeActionOneMinText = getString(R.string.dvr_recording_settings_time_one_min);
+ mTimeActionFiveMinText = getString(R.string.dvr_recording_settings_time_five_mins);
+ mTimeActionFifteenMinText = getString(R.string.dvr_recording_settings_time_fifteen_mins);
+ mTimeActionHalfHourText = getString(R.string.dvr_recording_settings_time_half_hour);
+ mTimeActionOneHourText = getString(R.string.dvr_recording_settings_time_one_hour);
+ mTimeActionTwoHoursText = getString(R.string.dvr_recording_settings_time_two_hours);
+ mTimeActionThreeHoursText = getString(R.string.dvr_recording_settings_time_three_hours);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String breadcrumb = mProgram.getTitle();
+ String title = mFragmentTitle;
+ String description = mProgram.getEpisodeTitle() + "\n" + mProgram.getDescription();
+ return new Guidance(title, description, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mStartEarlyGuidedAction =
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_START_EARLY)
+ .title(mStartEarlyActionTitle)
+ .description(mTimeActionOnTimeText)
+ .subActions(buildChannelSubActionStart())
+ .build();
+ actions.add(mStartEarlyGuidedAction);
+
+ mEndLateGuidedAction =
+ new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_END_LATE)
+ .title(mEndLateActionTitle)
+ .description(mTimeActionOnTimeText)
+ .subActions(buildChannelSubActionEnd())
+ .build();
+ actions.add(mEndLateGuidedAction);
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_OK)
+ .build());
+ actions.add(
+ new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == GuidedAction.ACTION_ID_OK) {
+ long startEarlyTimeMs = TimeUnit.MINUTES.toMillis(mStartEarlyTime);
+ long endLateTimeMs = TimeUnit.MINUTES.toMillis(mEndLateTime);
+ long startTimeMs = mProgram.getStartTimeUtcMillis() - startEarlyTimeMs;
+ if (startTimeMs < System.currentTimeMillis()) {
+ startTimeMs = System.currentTimeMillis();
+ startEarlyTimeMs = mProgram.getStartTimeUtcMillis() - startTimeMs;
+ }
+ long endTimeMs = mProgram.getEndTimeUtcMillis() + endLateTimeMs;
+ Program customizedProgram =
+ new ProgramImpl.Builder(mProgram)
+ .setStartTimeUtcMillis(startTimeMs)
+ .setEndTimeUtcMillis(endTimeMs)
+ .build();
+ mDvrManager.addSchedule(customizedProgram , startEarlyTimeMs, endLateTimeMs);
+ List<ScheduledRecording> conflicts =
+ mDvrManager.getConflictingSchedules(customizedProgram);
+ if (conflicts.isEmpty()) {
+ DvrUiHelper.showAddScheduleToast(
+ getContext(),
+ customizedProgram.getTitle(),
+ customizedProgram.getStartTimeUtcMillis(),
+ customizedProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ finishGuidedStepFragments();
+ } else {
+ DvrUiHelper.showScheduleConflictDialog(getActivity(), customizedProgram);
+ }
+ } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ finishGuidedStepFragments();
+ }
+ }
+
+ @Override
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ switch ((int) actionId) {
+ case SUB_ACTION_ID_START_ON_TIME :
+ mStartEarlyTime = 0;
+ updateGuidedActions(true, mTimeActionOnTimeText);
+ break;
+ case SUB_ACTION_ID_START_ONE_MIN :
+ mStartEarlyTime = 1;
+ updateGuidedActions(true, mTimeActionOneMinText);
+ break;
+ case SUB_ACTION_ID_START_FIVE_MIN :
+ mStartEarlyTime = 5;
+ updateGuidedActions(true, mTimeActionFiveMinText);
+ break;
+ case SUB_ACTION_ID_START_FIFTEEN_MIN :
+ mStartEarlyTime = 15;
+ updateGuidedActions(true, mTimeActionFifteenMinText);
+ break;
+ case SUB_ACTION_ID_START_HALF_HOUR :
+ mStartEarlyTime = 30;
+ updateGuidedActions(true, mTimeActionHalfHourText);
+ break;
+ case SUB_ACTION_ID_END_ON_TIME :
+ mEndLateTime = 0;
+ updateGuidedActions(false, mTimeActionOnTimeText);
+ break;
+ case SUB_ACTION_ID_END_ONE_MIN :
+ mEndLateTime = 1;
+ updateGuidedActions(false, mTimeActionOneMinText);
+ break;
+ case SUB_ACTION_ID_END_FIFTEEN_MIN :
+ mEndLateTime = 15;
+ updateGuidedActions(false, mTimeActionFifteenMinText);
+ break;
+ case SUB_ACTION_ID_END_HALF_HOUR :
+ mEndLateTime = 30;
+ updateGuidedActions(false, mTimeActionHalfHourText);
+ break;
+ case SUB_ACTION_ID_END_ONE_HOUR :
+ mEndLateTime = 60;
+ updateGuidedActions(false, mTimeActionOneHourText);
+ break;
+ case SUB_ACTION_ID_END_TWO_HOURS :
+ mEndLateTime = 120;
+ updateGuidedActions(false, mTimeActionTwoHoursText);
+ break;
+ case SUB_ACTION_ID_END_THREE_HOURS :
+ mEndLateTime = 180;
+ updateGuidedActions(false, mTimeActionThreeHoursText);
+ break;
+ default :
+ mStartEarlyTime = 0;
+ mEndLateTime = 0;
+ updateGuidedActions(true, mTimeActionOnTimeText);
+ updateGuidedActions(false, mTimeActionOnTimeText);
+ break;
+ }
+ return true;
+ }
+
+ private void updateGuidedActions(boolean start, CharSequence description) {
+ if (start) {
+ mStartEarlyGuidedAction.setDescription(description);
+ notifyActionChanged(findActionPositionById(ACTION_ID_START_EARLY));
+ } else {
+ mEndLateGuidedAction.setDescription(description);
+ notifyActionChanged(findActionPositionById(ACTION_ID_END_LATE));
+ }
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ return new DvrGuidedActionsStylist(true);
+ }
+
+ private List<GuidedAction> buildChannelSubActionStart() {
+ List<GuidedAction> timeSubActions = new ArrayList<>();
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_START_ON_TIME)
+ .title(mTimeActionOnTimeText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_START_ONE_MIN)
+ .title(mTimeActionOneMinText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_START_FIVE_MIN)
+ .title(mTimeActionFiveMinText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_START_FIFTEEN_MIN)
+ .title(mTimeActionFifteenMinText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_START_HALF_HOUR)
+ .title(mTimeActionHalfHourText)
+ .build());
+ return timeSubActions;
+ }
+
+ private List<GuidedAction> buildChannelSubActionEnd() {
+ List<GuidedAction> timeSubActions = new ArrayList<>();
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_ON_TIME)
+ .title(mTimeActionOnTimeText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_ONE_MIN)
+ .title(mTimeActionOneMinText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_FIFTEEN_MIN)
+ .title(mTimeActionFifteenMinText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_HALF_HOUR)
+ .title(mTimeActionHalfHourText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_ONE_HOUR)
+ .title(mTimeActionOneHourText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_TWO_HOURS)
+ .title(mTimeActionTwoHoursText)
+ .build());
+ timeSubActions.add(
+ new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_END_THREE_HOURS)
+ .title(mTimeActionThreeHoursText)
+ .build());
+ return timeSubActions;
+ }
+
+ protected void dismissDialog() {
+ if (getActivity() instanceof MainActivity) {
+ SafeDismissDialogFragment currentDialog =
+ ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
+ if (currentDialog instanceof DvrHalfSizedDialogFragment) {
+ currentDialog.dismiss();
+ }
+ } else if (getParentFragment() instanceof DialogFragment) {
+ ((DialogFragment) getParentFragment()).dismiss();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index 7131f626..249e63db 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -31,6 +31,7 @@ import androidx.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.data.ProgramImpl;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
@@ -40,6 +41,9 @@ import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import java.util.Collections;
import java.util.List;
+import javax.inject.Inject;
+import dagger.android.AndroidInjection;
+
/**
* A fragment which asks the user the type of the recording.
*
@@ -57,6 +61,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
private ProgramImpl mProgram;
private boolean mAddCurrentProgramToSeries;
+ @Inject DvrFlags mDvrFlags;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -81,6 +86,12 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
}
@Override
+ public void onAttach(Context context) {
+ AndroidInjection.inject(this);
+ super.onAttach(context);
+ }
+
+ @Override
public int onProvideTheme() {
return R.style.Theme_TV_Dvr_GuidedStep_Twoline_Action;
}
@@ -125,21 +136,28 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
@Override
public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_EPISODE) {
- getDvrManager().addSchedule(mProgram);
- List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules(mProgram);
- if (conflicts.isEmpty()) {
- DvrUiHelper.showAddScheduleToast(
- getContext(),
- mProgram.getTitle(),
- mProgram.getStartTimeUtcMillis(),
- mProgram.getEndTimeUtcMillis());
- dismissDialog();
+ if (mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
} else {
- GuidedStepFragment fragment = new DvrProgramConflictFragment();
- Bundle args = new Bundle();
- args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, mProgram);
- fragment.setArguments(args);
- GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ getDvrManager().addSchedule(mProgram);
+ List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules
+ (mProgram);
+
+ if (conflicts.isEmpty()) {
+ DvrUiHelper.showAddScheduleToast(
+ getContext(),
+ mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(),
+ mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ } else {
+ GuidedStepFragment fragment = new DvrProgramConflictFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, mProgram);
+ fragment.setArguments(args);
+ GuidedStepFragment.add(getFragmentManager(), fragment, R.id
+ .halfsized_dialog_host);
+ }
}
} else if (action.getId() == ACTION_RECORD_SERIES) {
SeriesRecording seriesRecording =
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index 657abfa2..1ab1d173 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -525,6 +525,22 @@ public class DvrUiHelper {
}
}
+ /**
+ * Shows the episode recording settings activity.
+ *
+ * @param program Program to be recorded
+ */
+ public static void startRecordingSettingsActivity(
+ Context context,
+ Program program) {
+ if (program != null) {
+ Intent intent = new Intent(context, DvrRecordingSettingsActivity.class);
+ intent.putExtra(DvrRecordingSettingsActivity.IS_WINDOW_TRANSLUCENT, true);
+ intent.putExtra(DvrRecordingSettingsActivity.PROGRAM, program.toParcelable());
+ context.startActivity(intent);
+ }
+ }
+
private static void startSeriesSettingsActivityInternal(
Context context,
long seriesRecordingId,
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 5ec293f7..b14d30ba 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -40,7 +40,9 @@ import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
+import com.android.tv.app.LiveTvApplication;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.common.util.Clock;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.api.Channel;
@@ -82,6 +84,7 @@ public class ProgramItemView extends TextView {
private final DvrManager mDvrManager;
@Inject Clock mClock;
@Inject ChannelDataManager mChannelDataManager;
+ @Inject DvrFlags mDvrFlags;
private ProgramGuide mProgramGuide;
private TableEntry mTableEntry;
private int mMaxWidthForRipple;
@@ -98,6 +101,7 @@ public class ProgramItemView extends TextView {
public void onClick(final View view) {
TableEntry entry = ((ProgramItemView) view).mTableEntry;
Clock clock = ((ProgramItemView) view).mClock;
+ DvrFlags dvrFlags = ((ProgramItemView) view).mDvrFlags;
if (entry == null) {
// do nothing
return;
@@ -126,12 +130,18 @@ public class ProgramItemView extends TextView {
if (entry.entryStartUtcMillis > clock.currentTimeMillis()
&& dvrManager.isProgramRecordable(entry.program)) {
if (entry.scheduledRecording == null) {
- DvrUiHelper.checkStorageStatusAndShowErrorMessage(
- tvActivity,
- channel.getInputId(),
- () ->
- DvrUiHelper.requestRecordingFutureProgram(
- tvActivity, entry.program, false));
+ if (!entry.program.isEpisodic() &&
+ dvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(view.getContext(),
+ entry.program);
+ } else {
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ tvActivity,
+ channel.getInputId(),
+ () ->
+ DvrUiHelper.requestRecordingFutureProgram(
+ tvActivity, entry.program, false));
+ }
} else {
dvrManager.removeScheduledRecording(entry.scheduledRecording);
String msg =
diff --git a/src/com/android/tv/modules/TvApplicationModule.java b/src/com/android/tv/modules/TvApplicationModule.java
index 99753d1e..1955ecf7 100644
--- a/src/com/android/tv/modules/TvApplicationModule.java
+++ b/src/com/android/tv/modules/TvApplicationModule.java
@@ -32,7 +32,12 @@ import com.android.tv.data.epg.EpgFetcherImpl;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManagerImpl;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture;
+import com.android.tv.dvr.provider.DvrDbSync;
+import com.android.tv.dvr.provider.DvrDbSyncFactory;
+import com.android.tv.dvr.provider.DvrQueryScheduleFutureFactory;
import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.onboarding.SetupSourcesFragment;
@@ -96,6 +101,12 @@ public abstract class TvApplicationModule {
return channelDataManager;
}
+ @Provides
+ @Singleton
+ static DvrManager providesDvrManager(@ApplicationContext Context context) {
+ return new DvrManager(context);
+ }
+
@Binds
@Singleton
abstract DvrDataManager providesDvrDataManager(DvrDataManagerImpl impl);
@@ -108,6 +119,13 @@ public abstract class TvApplicationModule {
@Singleton
abstract EpgFetcher epgFetcher(EpgFetcherImpl impl);
+ @Binds
+ abstract DvrDbSync.Factory dvrDbSyncFactory(DvrDbSyncFactory dvrDbSyncFactory);
+
+ @Binds
+ abstract DvrQueryScheduleFuture.Factory dvrQueryScheduleFutureFactory(
+ DvrQueryScheduleFutureFactory dvrQueryScheduleFutureFactory);
+
@ContributesAndroidInjector
abstract PinDialogFragment contributesPinDialogFragment();
diff --git a/src/com/android/tv/ui/DetailsActivity.java b/src/com/android/tv/ui/DetailsActivity.java
index 92c13f57..3accbf25 100644
--- a/src/com/android/tv/ui/DetailsActivity.java
+++ b/src/com/android/tv/ui/DetailsActivity.java
@@ -218,5 +218,8 @@ public class DetailsActivity extends DaggerActivity
@ContributesAndroidInjector
abstract CurrentRecordingDetailsFragment
contributesCurrentRecordingDetailsFragmentInjector();
+
+ @ContributesAndroidInjector
+ abstract ProgramDetailsFragment contributesProgramDetailsFragmentInjector();
}
}
diff --git a/src/com/android/tv/ui/ProgramDetailsFragment.java b/src/com/android/tv/ui/ProgramDetailsFragment.java
index bfcebd7d..49f8b8d8 100644
--- a/src/com/android/tv/ui/ProgramDetailsFragment.java
+++ b/src/com/android/tv/ui/ProgramDetailsFragment.java
@@ -39,6 +39,7 @@ import androidx.leanback.widget.VerticalGridView;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.common.flags.DvrFlags;
import com.android.tv.data.ProgramImpl;
import com.android.tv.data.api.Channel;
import com.android.tv.dvr.DvrDataManager;
@@ -52,6 +53,9 @@ import com.android.tv.dvr.ui.browse.DetailsContentPresenter;
import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper;
import com.android.tv.util.images.ImageLoader;
+import javax.inject.Inject;
+import dagger.android.AndroidInjection;
+
/** A fragment shows the details of a Program */
public class ProgramDetailsFragment extends DetailsFragment
implements DvrDataManager.ScheduledRecordingListener,
@@ -72,6 +76,7 @@ public class ProgramDetailsFragment extends DetailsFragment
private DvrManager mDvrManager;
private DvrDataManager mDvrDataManager;
private DvrScheduleManager mDvrScheduleManager;
+ @Inject DvrFlags mDvrFlags;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -82,6 +87,12 @@ public class ProgramDetailsFragment extends DetailsFragment
}
@Override
+ public void onAttach(Context context) {
+ AndroidInjection.inject(this);
+ super.onAttach(context);
+ }
+
+ @Override
public void onDestroy() {
mDvrDataManager.removeScheduledRecordingListener(this);
mDvrScheduleManager.removeOnConflictStateChangeListener(this);
@@ -216,12 +227,16 @@ public class ProgramDetailsFragment extends DetailsFragment
} else if (actionId == ACTION_CANCEL) {
mDvrManager.removeScheduledRecording(mScheduledRecording);
} else if (actionId == ACTION_SCHEDULE_RECORDING) {
- DvrUiHelper.checkStorageStatusAndShowErrorMessage(
- getActivity(),
- mInputId,
- () ->
- DvrUiHelper.requestRecordingFutureProgram(
- getActivity(), mProgram, false));
+ if (!mProgram.isEpisodic() && mDvrFlags.startEarlyEndLateEnabled()) {
+ DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
+ } else {
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(
+ getActivity(),
+ mInputId,
+ () ->
+ DvrUiHelper.requestRecordingFutureProgram(
+ getActivity(), mProgram, false));
+ }
}
}
};
diff --git a/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java b/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java
index 5f8db0ad..c7ae5240 100644
--- a/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java
+++ b/tests/robotests/src/com/android/tv/dvr/provider/DvrDbSyncTest.java
@@ -16,13 +16,18 @@
package com.android.tv.dvr.provider;
+import static com.google.common.truth.Truth.assertThat;
+import static java.lang.Math.abs;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.refEq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.android.tv.common.flags.DvrFlags;
+import com.android.tv.common.flags.impl.DefaultDvrFlags;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramImpl;
import com.android.tv.data.api.Program;
@@ -34,9 +39,11 @@ import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.testing.TestSingletonApp;
import com.android.tv.testing.constants.ConfigConstants;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@@ -44,14 +51,24 @@ import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.util.concurrent.RoboExecutorService;
import org.robolectric.annotation.Config;
+import java.util.concurrent.TimeUnit;
+
/** Tests for {@link com.android.tv.dvr.DvrScheduleManager} */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = ConfigConstants.SDK, application = TestSingletonApp.class)
public class DvrDbSyncTest {
private static final String INPUT_ID = "input_id";
private static final long BASE_PROGRAM_ID = 1;
- private static final long BASE_START_TIME_MS = 0;
- private static final long BASE_END_TIME_MS = 1;
+ private static final long BASE_TIME_MS =
+ System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1);
+ private static final long BASE_START_TIME_MS = BASE_TIME_MS;
+ private static final long BASE_END_TIME_MS = BASE_TIME_MS + 1;
+ private static final long BASE_OFFSET_TIME_MS = 1;
+ private static final long BASE_START_TIME_WITH_OFFSET_MS =
+ BASE_START_TIME_MS - BASE_OFFSET_TIME_MS;
+ private static final long BASE_END_TIME_WITH_OFFSET_MS = BASE_END_TIME_MS + BASE_OFFSET_TIME_MS;
+ private static final long BASE_LARGE_OFFSET_TIME_MS = TimeUnit.MINUTES.toMillis(2);
+ private static final long RECORD_MARGIN_MS = TimeUnit.SECONDS.toMillis(10);
private static final String BASE_SEASON_NUMBER = "2";
private static final String BASE_EPISODE_NUMBER = "3";
private ProgramImpl baseProgram;
@@ -61,6 +78,7 @@ public class DvrDbSyncTest {
private DvrDbSync mDbSync;
@Mock private DvrManager mDvrManager;
+ private DvrFlags mDvrFlags = new DefaultDvrFlags();
@Mock private WritableDvrDataManager mDataManager;
@Mock private ChannelDataManager mChannelDataManager;
@Mock private SeriesRecordingScheduler mSeriesRecordingScheduler;
@@ -93,6 +111,7 @@ public class DvrDbSyncTest {
new DvrDbSync(
RuntimeEnvironment.application.getApplicationContext(),
mDataManager,
+ mDvrFlags,
mChannelDataManager,
mDvrManager,
mSeriesRecordingScheduler,
@@ -166,6 +185,116 @@ public class DvrDbSyncTest {
verify(mDataManager, never()).updateScheduledRecording(any());
}
+ @Test
+ public void testHandleUpdateProgram_addOffsetNotStarted() {
+ Assume.assumeTrue(mDvrFlags.startEarlyEndLateEnabled());
+ ScheduledRecording schedule = ScheduledRecording.buildFrom(baseSchedule)
+ .setStartOffsetMs(BASE_OFFSET_TIME_MS)
+ .setEndOffsetMs(BASE_OFFSET_TIME_MS)
+ .build();
+ addSchedule(BASE_PROGRAM_ID, schedule);
+ mDbSync.handleUpdateProgram(baseProgram, BASE_PROGRAM_ID);
+ ScheduledRecording expectedSchedule =
+ ScheduledRecording.buildFrom(schedule)
+ .setStartTimeMs(BASE_START_TIME_WITH_OFFSET_MS)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS)
+ .build();
+ assertUpdateScheduleCalled(expectedSchedule);
+ }
+
+ @Test
+ public void testHandleUpdateProgram_addOffsetInProgress() {
+ Assume.assumeTrue(mDvrFlags.startEarlyEndLateEnabled());
+ ScheduledRecording schedule =
+ ScheduledRecording.buildFrom(baseSchedule)
+ .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)
+ .setStartOffsetMs(BASE_OFFSET_TIME_MS)
+ .setEndOffsetMs(BASE_OFFSET_TIME_MS)
+ .build();
+ addSchedule(BASE_PROGRAM_ID, schedule);
+ mDbSync.handleUpdateProgram(baseProgram, BASE_PROGRAM_ID);
+ ScheduledRecording expectedSchedule =
+ ScheduledRecording.buildFrom(schedule)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS)
+ .build();
+ assertUpdateScheduleCalled(expectedSchedule);
+ }
+
+ @Test
+ public void testHandleUpdateProgram_changeTimeNotStartedWithScheduleOffsets() {
+ Assume.assumeTrue(mDvrFlags.startEarlyEndLateEnabled());
+ ScheduledRecording schedule =
+ ScheduledRecording.buildFrom(baseSchedule)
+ .setStartTimeMs(BASE_START_TIME_WITH_OFFSET_MS)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS)
+ .setStartOffsetMs(BASE_OFFSET_TIME_MS)
+ .setEndOffsetMs(BASE_OFFSET_TIME_MS)
+ .build();
+ addSchedule(BASE_PROGRAM_ID, schedule);
+ long startTimeMs = BASE_START_TIME_MS + 1;
+ long endTimeMs = BASE_END_TIME_MS + 1;
+ Program program =
+ new ProgramImpl.Builder(baseProgram)
+ .setStartTimeUtcMillis(startTimeMs)
+ .setEndTimeUtcMillis(endTimeMs)
+ .build();
+ mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+ ScheduledRecording expectedSchedule =
+ ScheduledRecording.buildFrom(schedule)
+ .setStartTimeMs(BASE_START_TIME_WITH_OFFSET_MS + 1)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS + 1)
+ .build();
+ assertUpdateScheduleCalled(expectedSchedule);
+ }
+
+ @Test
+ public void testHandleUpdateProgram_changeTimeInProgressWithScheduleOffsets() {
+ Assume.assumeTrue(mDvrFlags.startEarlyEndLateEnabled());
+ ScheduledRecording schedule =
+ ScheduledRecording.buildFrom(baseSchedule)
+ .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS)
+ .setStartTimeMs(BASE_START_TIME_WITH_OFFSET_MS)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS)
+ .setStartOffsetMs(BASE_OFFSET_TIME_MS)
+ .setEndOffsetMs(BASE_OFFSET_TIME_MS)
+ .build();
+ addSchedule(BASE_PROGRAM_ID, schedule);
+ long startTimeMs = BASE_START_TIME_MS + 1;
+ long endTimeMs = BASE_END_TIME_MS + 1;
+ Program program =
+ new ProgramImpl.Builder(baseProgram)
+ .setStartTimeUtcMillis(startTimeMs)
+ .setEndTimeUtcMillis(endTimeMs)
+ .build();
+ mDbSync.handleUpdateProgram(program, BASE_PROGRAM_ID);
+ ScheduledRecording expectedSchedule =
+ ScheduledRecording.buildFrom(schedule)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS + 1)
+ .build();
+ assertUpdateScheduleCalled(expectedSchedule);
+ }
+
+ @Test
+ public void testHandleUpdateProgram_addStartTimeInPastOffsetNotStarted() {
+ Assume.assumeTrue(mDvrFlags.startEarlyEndLateEnabled());
+ ScheduledRecording schedule =
+ ScheduledRecording.buildFrom(baseSchedule)
+ .setStartOffsetMs(BASE_LARGE_OFFSET_TIME_MS)
+ .setEndOffsetMs(BASE_OFFSET_TIME_MS)
+ .build();
+ addSchedule(BASE_PROGRAM_ID, schedule);
+ mDbSync.handleUpdateProgram(baseProgram, BASE_PROGRAM_ID);
+ long startTimeMs = System.currentTimeMillis() + RECORD_MARGIN_MS;
+ long startOffsetMs = BASE_START_TIME_MS - startTimeMs;
+ ScheduledRecording expectedSchedule =
+ ScheduledRecording.buildFrom(schedule)
+ .setStartTimeMs(startTimeMs)
+ .setStartOffsetMs(startOffsetMs)
+ .setEndTimeMs(BASE_END_TIME_WITH_OFFSET_MS)
+ .build();
+ assertUpdateScheduleCalledWithinRange(expectedSchedule, RECORD_MARGIN_MS);
+ }
+
private void addSchedule(long programId, ScheduledRecording schedule) {
when(mDataManager.getScheduledRecordingForProgramId(programId)).thenReturn(schedule);
}
@@ -175,4 +304,25 @@ public class DvrDbSyncTest {
.updateScheduledRecording(
eq(ScheduledRecording.builder(INPUT_ID, program).build()));
}
+
+ private void assertUpdateScheduleCalled(ScheduledRecording schedule) {
+ verify(mDataManager).updateScheduledRecording(eq(schedule));
+ }
+
+ private void assertUpdateScheduleCalledWithinRange(ScheduledRecording schedule, long range) {
+ // Compare the schedules excluding startTimeMs and startOffsetMs
+ verify(mDataManager).updateScheduledRecording(
+ refEq(schedule,"start_time_utc_millis", "start_offset_millis"));
+ // Fetch the actual schedule
+ ArgumentCaptor<ScheduledRecording> actualArgument =
+ ArgumentCaptor.forClass(ScheduledRecording.class);
+ verify(mDataManager).updateScheduledRecording(actualArgument.capture());
+ ScheduledRecording actualSchedule = actualArgument.getValue();
+ // Assert that values of startTimeMs and startOffsetMs are within expected range
+ long startTimeDelta = abs(actualSchedule.getStartTimeMs() - schedule.getStartTimeMs());
+ long startOffsetDelta =
+ abs(actualSchedule.getStartOffsetMs() - schedule.getStartOffsetMs());
+ assertThat(startTimeDelta).isAtMost(range);
+ assertThat(startOffsetDelta).isAtMost(range);
+ }
}