diff options
author | Shraddha Basantwani <shraddha.basantwani@ittiam.com> | 2019-02-26 16:03:03 +0530 |
---|---|---|
committer | Shraddha Basantwani <shraddha.basantwani@ittiam.com> | 2019-11-18 13:32:44 +0530 |
commit | fc3f2d9eadeb434e4e77253ece65e152779e9589 (patch) | |
tree | a9b446823f68287accb253d3d0dd5ed0d48846e6 | |
parent | 345d5fdf28c7316d522daa242e452b06473e5dac (diff) | |
download | TV-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
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); + } } |