diff options
Diffstat (limited to 'src/com/android/tv/dvr')
91 files changed, 2680 insertions, 1348 deletions
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index 89661df3..a8637449 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -26,7 +26,10 @@ import android.util.Log; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; @@ -318,5 +321,41 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override + public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) { + List<SeriesRecording> toRemove = new ArrayList<>(); + for (long rId : seriesRecordingIds) { + SeriesRecording seriesRecording = getSeriesRecording(rId); + if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) { + toRemove.add(seriesRecording); + } + } + removeSeriesRecording(SeriesRecording.toArray(toRemove)); + } + + /** + * Returns {@code true}, if the series recording is empty and can be removed. If a series + * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be + * removed. + */ + protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) { + if (!seriesRecording.isStopped()) { + return false; + } + long seriesRecordingId = seriesRecording.getId(); + for (ScheduledRecording r : getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + return false; + } + } + String seriesId = seriesRecording.getSeriesId(); + for (RecordedProgram r : getRecordedPrograms()) { + if (seriesId.equals(r.getSeriesId())) { + return false; + } + } + return true; + } + + @Override public void forgetStorage(String inputId) { } } diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 06613667..6d400b82 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -21,7 +21,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Range; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import java.util.Collection; import java.util.List; @@ -211,6 +214,13 @@ public interface DvrDataManager { Collection<Long> getDisallowedProgramIds(); /** + * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains + * any available schedules or recorded programs, and it's status is + * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. + */ + void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds); + + /** * Listens for the DVR schedules loading finished. */ interface OnDvrScheduleLoadFinishedListener { diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 46682a48..6d0a9959 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -42,7 +42,11 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.IdGenerator; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask; @@ -51,6 +55,8 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; +import com.android.tv.dvr.provider.DvrDbSync; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; @@ -267,11 +273,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); + if (mRecordedProgramLoadFinished) { + validateSeriesRecordings(); + } mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); SeriesRecordingScheduler.getInstance(mContext).start(); } } @@ -306,6 +315,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (uri == null) { uri = RecordedPrograms.CONTENT_URI; } + if (recordedPrograms == null) { + recordedPrograms = Collections.emptyList(); + } int match = TvProviderUriMatcher.match(uri); if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { @@ -318,7 +330,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); - } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); + } + } else if (recordedPrograms.isEmpty()) { List<RecordedProgram> oldRecordedPrograms = new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); @@ -355,6 +371,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (isInitialized()) { + validateSeriesRecordings(); SeriesRecordingScheduler.getInstance(mContext).start(); } } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { @@ -363,11 +380,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); - if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (recordedPrograms.isEmpty()) { mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { notifyRecordedProgramsRemoved(old); + SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId()); + if (r != null && isEmptySeriesRecording(r)) { + removeSeriesRecording(r); + } } } else { RecordedProgram recordedProgram = recordedPrograms.get(0); @@ -592,10 +613,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); + Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - getDeletedScheduleMap().remove(r.getId()); + getDeletedScheduleMap().remove(r.getProgramId()); mProgramId2ScheduledRecordings.remove(r.getProgramId()); + if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } boolean isScheduleForRemovedInput = mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; // If it belongs to the series recording and it's not started yet, just mark delete @@ -614,8 +641,19 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (mDvrLoadFinished) { + if (mRecordedProgramLoadFinished) { + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); + } notifyScheduledRecordingRemoved(schedules); } + Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator(); + while (iterator.hasNext()) { + ScheduledRecording r = iterator.next(); + if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) { + iterator.remove(); + schedulesToDelete.add(r); + } + } if (!schedulesToDelete.isEmpty()) { new AsyncDeleteScheduleTask(mContext).executeOnDbThread( ScheduledRecording.toArray(schedulesToDelete)); @@ -669,6 +707,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List<ScheduledRecording> toUpdate = new ArrayList<>(); + Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, "Recording not found for: " + r)) { @@ -691,6 +730,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (programId != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(programId, r); } + if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + // If the scheduled recording is failed, it may cause the automatically generated + // series recording for this schedule becomes invalid (with no future schedules and + // past recordings.) We should check and remove these series recordings. + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } } if (toUpdate.isEmpty()) { return; @@ -702,12 +748,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { + if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG, + "Non Existing Series ID: " + r)) { + continue; + } SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" @@ -769,14 +820,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); - List<SeriesRecording> movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, new Filter<RecordedProgram>() { @@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); + List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + if (r.getInputId().equals(inputId)) { + if (!isEmptySeriesRecording(r)) { + return true; + } + removedSeriesRecordings.add(r); + } + return false; + } + }); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); @@ -795,6 +853,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); } } + for (SeriesRecording r : removedSeriesRecordings) { + mSeriesRecordingsForRemovedInput.remove(r.getId()); + } + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( + SeriesRecording.toArray(removedSeriesRecordings)); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -811,20 +874,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (DEBUG) Log.d(TAG, "hideInput " + inputId); List<ScheduledRecording> movedSchedules = moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter<ScheduledRecording>() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); List<SeriesRecording> movedSeriesRecordings = moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, new Filter<RecordedProgram>() { @@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) { + int i = 0; + long[] rIds = new long[seriesRecordingIds.size()]; + for (long rId : seriesRecordingIds) { + rIds[i++] = rId; + } + checkAndRemoveEmptySeriesRecording(rIds); + } + @Override public void forgetStorage(String inputId) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); @@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }.executeOnDbThread(); } + private void validateSeriesRecordings() { + Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator(); + List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); + while (iter.hasNext()) { + SeriesRecording r = iter.next(); + if (isEmptySeriesRecording(r)) { + iter.remove(); + removedSeriesRecordings.add(r); + } + } + if (!removedSeriesRecordings.isEmpty()) { + SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed); + if (mDvrLoadFinished) { + notifySeriesRecordingRemoved(removed); + } + } + } + private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 5fa6f90f..2effe9cf 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -46,7 +46,9 @@ import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; -import com.android.tv.dvr.SeriesRecording.SeriesState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; @@ -234,7 +236,7 @@ public class DvrManager { * Adds a new series recording and schedules for the programs with the initial state. */ public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule, @SeriesState int initialState) { + List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) { Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { @@ -308,8 +310,7 @@ public class DvrManager { ScheduledRecording scheduleWithSameProgram = mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { - if (scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + if (scheduleWithSameProgram.isNotStarted()) { ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) .setSeriesRecordingId(series.getId()) .build(); @@ -337,10 +338,10 @@ public class DvrManager { */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext); - scheduler.pauseUpdate(); SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); if (previousSeries != null) { + // If the channel option of series changed, remove the existing schedules. The new + // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment. if (previousSeries.getChannelOption() != series.getChannelOption() || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE && previousSeries.getChannelId() != series.getChannelId())) { @@ -350,6 +351,18 @@ public class DvrManager { for (ScheduledRecording schedule : schedules) { if (schedule.isNotStarted()) { schedulesToRemove.add(schedule); + } else if (schedule.isInProgress() + && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && schedule.getChannelId() != series.getChannelId()) { + stopRecording(schedule); + } + } + List<ScheduledRecording> deletedSchedules = + new ArrayList<>(mDataManager.getDeletedSchedules()); + for (ScheduledRecording deletedSchedule : deletedSchedules) { + if (deletedSchedule.getSeriesRecordingId() == series.getId() + && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) { + schedulesToRemove.add(deletedSchedule); } } mDataManager.removeScheduledRecording(true, @@ -363,7 +376,7 @@ public class DvrManager { List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(series.getId())) { - if (schedule.isNotStarted()) { + if (schedule.isNotStarted() || schedule.isInProgress()) { schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) .setPriority(priority).build()); } @@ -373,7 +386,6 @@ public class DvrManager { ScheduledRecording.toArray(schedulesToUpdate)); } } - scheduler.resumeUpdate(); } } @@ -400,33 +412,6 @@ public class DvrManager { } /** - * Returns true, if the series recording can be removed. If a series recording is NORMAL state - * or has recordings or schedules, it cannot be removed. - */ - public boolean canRemoveSeriesRecording(long seriesRecordingId) { - SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId); - if (seriesRecording == null) { - return false; - } - if (!seriesRecording.isStopped()) { - return false; - } - for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { - if (r.getSeriesRecordingId() == seriesRecordingId) { - return false; - } - } - String seriesId = seriesRecording.getSeriesId(); - SoftPreconditions.checkNotNull(seriesId); - for (RecordedProgram r : mDataManager.getRecordedPrograms()) { - if (seriesId.equals(r.getSeriesId())) { - return false; - } - } - return true; - } - - /** * Stops the currently recorded program */ public void stopRecording(final ScheduledRecording recording) { @@ -657,6 +642,9 @@ public class DvrManager { if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) { return false; } + if (channel.isRecordingProhibited()) { + return false; + } TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + channel); @@ -680,7 +668,12 @@ public class DvrManager { if (!mDataManager.isInitialized()) { return false; } - TvInputInfo info = Utils.getTvInputInfoForProgram(mAppContext, program); + Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager() + .getChannel(program.getChannelId()); + if (channel == null || channel.isRecordingProhibited()) { + return false; + } + TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + program); return false; @@ -733,6 +726,17 @@ public class DvrManager { return mDataManager.getSeriesRecording(program.getSeriesId()); } + /** + * Returns if there are valid items. Valid item contains {@link RecordedProgram}, + * available {@link ScheduledRecording} and {@link SeriesRecording}. + */ + public boolean hasValidItems() { + return !(mDataManager.getRecordedPrograms().isEmpty() + && mDataManager.getStartedRecordings().isEmpty() + && mDataManager.getNonStartedScheduledRecordings().isEmpty() + && mDataManager.getSeriesRecordings().isEmpty()); + } + @WorkerThread @VisibleForTesting // Should be public to use mock DvrManager object. @@ -840,9 +844,10 @@ public class DvrManager { } /** - * Listener internally used inside dvr package. + * Listener to stop recording request. Should only be internally used inside dvr and its + * sub-package. */ - interface Listener { + public interface Listener { void onStopRecordingRequested(ScheduledRecording scheduledRecording); } } diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index a5851a75..b72117aa 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,7 +24,6 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; -import android.util.LongSparseArray; import android.util.Range; import com.android.tv.ApplicationSingletons; @@ -35,7 +34,10 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.recorder.InputTaskScheduler; import com.android.tv.util.CompositeComparator; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -88,9 +90,8 @@ public class DvrScheduleManager { private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); // The inner map is a hash map from scheduled recording to its conflicting status, i.e., // the boolean value true denotes the schedule is just partially conflicting, which means - // although there's conflictit, it might still be recorded partially. - private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap = - new HashMap<>(); + // although there's conflict, it might still be recorded partially. + private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>(); private boolean mInitialized; @@ -171,10 +172,9 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } - Map<ScheduledRecording, Boolean> conflictInfo = - mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - conflictInfo.remove(schedule); + conflictInfo.remove(schedule.getId()); if (conflictInfo.isEmpty()) { mInputConflictInfoMap.remove(inputId); } @@ -221,21 +221,11 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } // Update conflict list as well - Map<ScheduledRecording, Boolean> conflictInfo = - mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - // Compare ID because ScheduledRecording.equals() doesn't work if the state - // is changed. - ScheduledRecording oldSchedule = null; - for (ScheduledRecording s : conflictInfo.keySet()) { - if (s.getId() == schedule.getId()) { - oldSchedule = s; - break; - } - } - if (oldSchedule != null) { - conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); - conflictInfo.remove(oldSchedule); + ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); + if (oldConflictInfo != null) { + oldConflictInfo.schedule = schedule; } } } @@ -317,24 +307,25 @@ public class DvrScheduleManager { List<ScheduledRecording> addedConflicts = new ArrayList<>(); List<ScheduledRecording> removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId); Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); - if (oldConflictsInfo != null) { - for (ScheduledRecording r : oldConflictsInfo.keySet()) { - oldConflictMap.put(r.getId(), r); + if (oldConflictInfo != null) { + for (ConflictInfo conflictInfo : oldConflictInfo.values()) { + oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule); } } - Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); - if (conflictInfo.isEmpty()) { + List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId); + if (conflicts.isEmpty()) { mInputConflictInfoMap.remove(inputId); } else { - mInputConflictInfoMap.put(inputId, conflictInfo); - List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet()); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map<Long, ConflictInfo> conflictInfos = new HashMap<>(); + for (ConflictInfo conflictInfo : conflicts) { + conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo); + if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) { + addedConflicts.add(conflictInfo.schedule); } } + mInputConflictInfoMap.put(inputId, conflictInfos); } removedConflicts.addAll(oldConflictMap.values()); } @@ -565,8 +556,7 @@ public class DvrScheduleManager { } /** - * Returns list of all conflicting scheduled recordings with schedules belonging to {@code - * seriesRecording} + * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording} * recording. * <p> * Any empty list means there is no conflicts. @@ -581,9 +571,18 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( + List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings( seriesRecording.getId()); - return getConflictingSchedules(input, schedulesForSeries); + List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>(); + for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) { + if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) { + availableScheduledRecordingForSeries.add(scheduledRecording); + } + } + if (availableScheduledRecordingForSeries.isEmpty()) { + return Collections.emptyList(); + } + return getConflictingSchedules(input, availableScheduledRecordingForSeries); } /** @@ -617,16 +616,16 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) { + private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); if (!mInitialized || input == null) { - return Collections.emptyMap(); + return Collections.emptyList(); } List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyMap(); + return Collections.emptyList(); } return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } @@ -645,8 +644,8 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.containsKey(schedule); + Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule.getId()); } /** @@ -664,8 +663,12 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.getOrDefault(schedule, false); + Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); + if (conflicts != null) { + ConflictInfo conflictInfo = conflicts.get(schedule.getId()); + return conflictInfo != null && conflictInfo.partialConflict; + } + return false; } /** @@ -813,15 +816,17 @@ public class DvrScheduleManager { @VisibleForTesting static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { - List<ScheduledRecording> result = new ArrayList<>( - getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); - Collections.sort(result, RESULT_COMPARATOR); + List<ScheduledRecording> result = new ArrayList<>(); + for (ConflictInfo conflictInfo : + getConflictingSchedulesInfo(schedules, tunerCount, periods)) { + result.add(conflictInfo.schedule); + } return result; } @VisibleForTesting - static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( - List<ScheduledRecording> schedules, int tunerCount) { + static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules, + int tunerCount) { return getConflictingSchedulesInfo(schedules, tunerCount, null); } @@ -836,13 +841,13 @@ public class DvrScheduleManager { * to be partially recorded under the given schedules and tuner count {@code true}, * or not {@code false}. */ - private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + private static List<ConflictInfo> getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); // Sort by the same order as that in InputTaskScheduler. Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); List<ScheduledRecording> recordings = new ArrayList<>(); - Map<ScheduledRecording, Boolean> conflicts = new HashMap<>(); + Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>(); Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); // Simulate InputTaskScheduler. while (!schedulesToCheck.isEmpty()) { @@ -853,26 +858,29 @@ public class DvrScheduleManager { if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); if (candidate != null) { if (!modified2OriginalSchedules.containsKey(candidate)) { - conflicts.put(candidate, true); + conflicts.put(candidate, new ConflictInfo(candidate, true)); } recordings.remove(candidate); recordings.add(schedule); if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = + modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { if (!modified2OriginalSchedules.containsKey(schedule)) { // if schedule has been modified, it's already conflicted. // No need to add it again. - conflicts.put(schedule, false); + conflicts.put(schedule, new ConflictInfo(schedule, false)); } long earliestEndTime = getEarliestEndTime(recordings); if (earliestEndTime < schedule.getEndTimeMs()) { @@ -912,7 +920,14 @@ public class DvrScheduleManager { } } } - return conflicts; + List<ConflictInfo> result = new ArrayList<>(conflicts.values()); + Collections.sort(result, new Comparator<ConflictInfo>() { + @Override + public int compare(ConflictInfo lhs, ConflictInfo rhs) { + return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); + } + }); + return result; } private static void removeFinishedRecordings(List<ScheduledRecording> recordings, @@ -954,6 +969,17 @@ public class DvrScheduleManager { return earliest; } + @VisibleForTesting + static class ConflictInfo { + public ScheduledRecording schedule; + public boolean partialConflict; + + ConflictInfo(ScheduledRecording schedule, boolean partialConflict) { + this.schedule = schedule; + this.partialConflict = partialConflict; + } + } + /** * A listener which is notified the initialization of schedule manager. */ @@ -970,6 +996,9 @@ public class DvrScheduleManager { public interface OnConflictStateChangeListener { /** * Called when the conflicting schedules change. + * <p> + * Note that this can be called before + * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called. * * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise * {@code false}. diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java index a653b5f4..2d41d732 100644 --- a/src/com/android/tv/dvr/DvrStorageStatusManager.java +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -25,6 +25,7 @@ import android.content.IntentFilter; import android.content.OperationApplicationException; import android.database.Cursor; import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; @@ -36,8 +37,11 @@ import android.support.annotation.IntDef; import android.support.annotation.WorkerThread; import android.util.Log; +import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.tuner.tvinput.TunerTvInputService; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.io.File; @@ -294,7 +298,7 @@ public class DvrStorageStatusManager { storageMounted, storageMountedDir, storageMountedCapacity); } - private class CleanUpDbTask extends AsyncTask<Void, Void, Void> { + private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> { private final ContentResolver mContentResolver; private CleanUpDbTask() { @@ -302,13 +306,15 @@ public class DvrStorageStatusManager { } @Override - protected Void doInBackground(Void... params) { + protected Boolean doInBackground(Void... params) { @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { return null; } - List<ContentProviderOperation> ops = getDeleteOps(storageStatus - == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL); + if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + return true; + } + List<ContentProviderOperation> ops = getDeleteOps(); if (ops == null || ops.isEmpty()) { return null; } @@ -329,13 +335,28 @@ public class DvrStorageStatusManager { } @Override - protected void onPostExecute(Void result) { + protected void onPostExecute(Boolean forgetStorage) { + if (forgetStorage != null && forgetStorage == true) { + DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager(); + TvInputManagerHelper tvInputManagerHelper = + TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + List<TvInputInfo> tvInputInfoList = + tvInputManagerHelper.getTvInputInfos(true, false); + if (tvInputInfoList == null || tvInputInfoList.isEmpty()) { + return; + } + for (TvInputInfo info : tvInputInfoList) { + if (Utils.isBundledInput(info.getId())) { + dvrManager.forgetStorage(info.getId()); + } + } + } if (mCleanUpDbTask == this) { mCleanUpDbTask = null; } } - private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) { + private List<ContentProviderOperation> getDeleteOps() { List<ContentProviderOperation> ops = new ArrayList<>(); try (Cursor c = mContentResolver.query( @@ -364,7 +385,7 @@ public class DvrStorageStatusManager { continue; } File recordedProgramDir = new File(dataUri.getPath()); - if (deleteAll || !recordedProgramDir.exists()) { + if (!recordedProgramDir.exists()) { ops.add(ContentProviderOperation.newDelete( TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); } diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index 4eada742..dc05ed06 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -22,6 +22,7 @@ import android.media.tv.TvInputManager; import android.support.annotation.IntDef; import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.dvr.data.RecordedProgram; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index bf72d912..129ba153 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -18,7 +18,9 @@ package com.android.tv.dvr; import android.support.annotation.MainThread; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; /** * Full data manager. @@ -27,7 +29,7 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState; * for internal use only. Do not call them from UI directly. */ @MainThread -interface WritableDvrDataManager extends DvrDataManager { +public interface WritableDvrDataManager extends DvrDataManager { /** * Adds new recordings. */ diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java index 0ed6362c..2ade1dad 100644 --- a/src/com/android/tv/dvr/IdGenerator.java +++ b/src/com/android/tv/dvr/data/IdGenerator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java index dd744f80..18e1c769 100644 --- a/src/com/android/tv/dvr/RecordedProgram.java +++ b/src/com/android/tv/dvr/data/RecordedProgram.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import static android.media.tv.TvContract.RecordedPrograms; diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java index 2bda10ea..88849a2c 100644 --- a/src/com/android/tv/dvr/ScheduledRecording.java +++ b/src/com/android/tv/dvr/data/ScheduledRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import android.content.ContentValues; import android.content.Context; @@ -27,9 +27,11 @@ import android.text.TextUtils; import android.util.Range; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; @@ -683,6 +685,19 @@ public final class ScheduledRecording implements Parcelable { } } + /** + * Returns the program's display title, if the program title is not null, returns program title. + * Otherwise returns the channel name. + */ + public String getProgramDisplayTitle(Context context) { + if (!TextUtils.isEmpty(mProgramTitle)) { + return mProgramTitle; + } + Channel channel = TvApplication.getSingletons(context).getChannelDataManager() + .getChannel(mChannelId); + return channel != null ? channel.getDisplayName() + : context.getString(R.string.no_program_information); + } /** * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java new file mode 100644 index 00000000..89533dbb --- /dev/null +++ b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.data; + +import android.text.TextUtils; + +import java.util.Objects; + +/** + * A plain java object which includes the season/episode number for the series recording. + */ +public class SeasonEpisodeNumber { + public final long seriesRecordingId; + public final String seasonNumber; + public final String episodeNumber; + + /** + * Creates a new Builder with the values set from an existing {@link ScheduledRecording}. + */ + public SeasonEpisodeNumber(ScheduledRecording r) { + this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); + } + + public SeasonEpisodeNumber(long seriesRecordingId, String seasonNumber, String episodeNumber) { + this.seriesRecordingId = seriesRecordingId; + this.seasonNumber = seasonNumber; + this.episodeNumber = episodeNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SeasonEpisodeNumber) + || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) { + return false; + } + SeasonEpisodeNumber that = (SeasonEpisodeNumber) o; + return seriesRecordingId == that.seriesRecordingId + && Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber); + } + + @Override + public int hashCode() { + return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); + } + + @Override + public String toString() { + return "SeasonEpisodeNumber{" + + "seriesRecordingId=" + seriesRecordingId + + ", seasonNumber='" + seasonNumber + + ", episodeNumber=" + episodeNumber + + '}'; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java index 30256dc5..a0dec4a4 100644 --- a/src/com/android/tv/dvr/SeriesInfo.java +++ b/src/com/android/tv/dvr/data/SeriesInfo.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; /** * Series information. diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java index f0690f5f..b7cf0f66 100644 --- a/src/com/android/tv/dvr/SeriesRecording.java +++ b/src/com/android/tv/dvr/data/SeriesRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import android.content.ContentValues; import android.database.Cursor; @@ -26,6 +26,7 @@ import android.text.TextUtils; import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index 1a12fb23..c5383d02 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -21,8 +21,8 @@ import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +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.NamedThreadFactory; diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 2f16ba5d..8b9481a9 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -27,8 +27,8 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +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; diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java index df181455..8a0c2d19 100644 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.provider; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -34,6 +34,11 @@ import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManagerImpl; +import com.android.tv.dvr.DvrManager; +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.TvProviderUriMatcher; @@ -57,11 +62,12 @@ import java.util.Set; */ @MainThread @TargetApi(Build.VERSION_CODES.N) -class DvrDbSync { +public class DvrDbSync { private static final String TAG = "DvrDbSync"; private static final boolean DEBUG = false; private final Context mContext; + private final DvrManager mDvrManager; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); @@ -129,17 +135,21 @@ class DvrDbSync { } }; - DvrDbSync(Context context, DvrDataManagerImpl dataManager) { - this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); + public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(), + TvApplication.getSingletons(context).getDvrManager(), + SeriesRecordingScheduler.getInstance(context)); } @VisibleForTesting DvrDbSync(Context context, DvrDataManagerImpl dataManager, - ChannelDataManager channelDataManager) { + ChannelDataManager channelDataManager, DvrManager dvrManager, + SeriesRecordingScheduler seriesRecordingScheduler) { mContext = context; + mDvrManager = dvrManager; mDataManager = dataManager; mChannelDataManager = channelDataManager; - mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); + mSeriesRecordingScheduler = seriesRecordingScheduler; } /** @@ -279,10 +289,9 @@ class DvrDbSync { mDataManager.getSeriesRecording(program.getSeriesId()); if (seriesRecording == null) { // The new program is episodic while the previous one isn't. - SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext) - .getDvrManager().addSeriesRecording(program, - Collections.singletonList(program), - SeriesRecording.STATE_SERIES_STOPPED); + SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording( + program, Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); builder.setSeriesRecordingId(newSeriesRecording.getId()); needUpdate = true; } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { @@ -306,8 +315,9 @@ class DvrDbSync { // Old program belongs to a series but the new one doesn't. seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); } - // Change start time only when the recording start time has not passed. - boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs + // Change start time only when the recording is not started yet. + boolean needToChangeStartTime = + schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { builder.setStartTimeMs(program.getStartTimeUtcMillis()); diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java index 15ca2700..ba0aca51 100644 --- a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java +++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.provider; import android.annotation.TargetApi; import android.content.Context; @@ -24,13 +24,15 @@ import android.media.tv.TvContract.Programs; import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; -import android.text.TextUtils; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; import com.android.tv.util.AsyncDbTask.CursorFilter; import com.android.tv.util.PermissionUtils; @@ -40,7 +42,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; /** @@ -253,21 +254,13 @@ abstract public class EpisodicProgramLoadTask { return sqlParams; } - @VisibleForTesting - static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes, - ScheduledEpisode episode) { - // The episode whose season number or episode number is null will always be scheduled. - return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber) - && !TextUtils.isEmpty(episode.episodeNumber); - } - /** * Filter the programs which match the series recording. The episodes which the schedules are * already created for are filtered out too. */ private class SeriesRecordingCursorFilter implements CursorFilter { private final Set<Long> mDisallowedProgramIds = new HashSet<>(); - private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); + private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>(); SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { if (!mLoadDisallowedProgram) { @@ -282,7 +275,7 @@ abstract public class EpisodicProgramLoadTask { if (seriesRecordingIds.contains(r.getSeriesRecordingId()) && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mScheduledEpisodes.add(new ScheduledEpisode(r)); + mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r)); } } } @@ -306,9 +299,9 @@ abstract public class EpisodicProgramLoadTask { } if (programMatches) { return mLoadScheduledEpisode - || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber())); + || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); } } return false; @@ -333,50 +326,4 @@ abstract public class EpisodicProgramLoadTask { public String[] selectionArgs; public CursorFilter filter; } - - /** - * A plain java object which includes the season/episode number for the series recording. - */ - public static class ScheduledEpisode { - public final long seriesRecordingId; - public final String seasonNumber; - public final String episodeNumber; - - /** - * Create a new Builder with the values set from an existing {@link ScheduledRecording}. - */ - ScheduledEpisode(ScheduledRecording r) { - this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); - } - - public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { - this.seriesRecordingId = seriesRecordingId; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ScheduledEpisode)) return false; - ScheduledEpisode that = (ScheduledEpisode) o; - return seriesRecordingId == that.seriesRecordingId - && Objects.equals(seasonNumber, that.seasonNumber) - && Objects.equals(episodeNumber, that.episodeNumber); - } - - @Override - public int hashCode() { - return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); - } - - @Override - public String toString() { - return "ScheduledEpisode{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; - } - } } diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java index 201e379e..8aa90116 100644 --- a/src/com/android/tv/dvr/ConflictChecker.java +++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.TargetApi; import android.content.ContentUris; @@ -37,6 +37,9 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java index 8c40aaa8..08ffaf86 100644 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java @@ -14,9 +14,10 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.app.AlarmManager; +import android.app.Notification; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -27,9 +28,13 @@ import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.tv.ApplicationSingletons; +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener; +import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.dvr.WritableDvrDataManager; import com.android.tv.util.Clock; import com.android.tv.util.RecurringRunner; @@ -52,6 +57,8 @@ public class DvrRecordingService extends Service { private static final boolean DEBUG = false; public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler"; + private static final int ONGOING_NOTIFICATION_ID = 1; + public static void startService(Context context) { Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); context.startService(dvrSchedulerIntent); @@ -62,6 +69,27 @@ public class DvrRecordingService extends Service { private Scheduler mScheduler; private HandlerThread mHandlerThread; + private InputSessionManager mSessionManager; + private boolean mForeground; + + private final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = + new OnRecordingSessionChangeListener() { + @Override + public void onRecordingSessionChange(final boolean create, final int count) { + if (create && !mForeground) { + Notification notification = + new Notification.Builder(getApplicationContext()) + .setContentTitle(TAG) + .setSmallIcon(R.drawable.ic_dvr) + .build(); + startForeground(ONGOING_NOTIFICATION_ID, notification); + mForeground = true; + } else if (!create && mForeground && count == 0) { + stopForeground(STOP_FOREGROUND_REMOVE); + mForeground = false; + } + } + }; @Override public void onCreate() { @@ -70,8 +98,11 @@ public class DvrRecordingService extends Service { super.onCreate(); SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); ApplicationSingletons singletons = TvApplication.getSingletons(this); - WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); + WritableDvrDataManager dataManager = + (WritableDvrDataManager) singletons.getDvrDataManager(); + mSessionManager = singletons.getInputSessionManager(); + mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); // mScheduler may have been set for testing. if (mScheduler == null) { @@ -105,6 +136,7 @@ public class DvrRecordingService extends Service { mHandlerThread.quitSafely(); mHandlerThread = null; } + mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener); super.onDestroy(); } diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java index 6d2f0d43..8c6ee145 100644 --- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java +++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import com.android.tv.TvApplication; diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java index 53c89ebc..46546a76 100644 --- a/src/com/android/tv/dvr/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.content.Context; import android.media.tv.TvInputInfo; @@ -30,6 +30,10 @@ import android.util.LongSparseArray; import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.CompositeComparator; diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java index c3d236b0..c3314dde 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.TargetApi; import android.content.Context; @@ -37,7 +37,10 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; -import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; @@ -256,13 +259,21 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback public void run() { if (TvApplication.getSingletons(mContext).getMainActivityWrapper() .isResumed()) { - Toast.makeText(mContext.getApplicationContext(), - R.string.dvr_error_insufficient_space_description, - Toast.LENGTH_LONG) - .show(); + ScheduledRecording scheduledRecording = mDataManager + .getScheduledRecording(mScheduledRecording.getId()); + if (scheduledRecording != null) { + Toast.makeText(mContext.getApplicationContext(), + mContext.getString(R.string + .dvr_error_insufficient_space_description_one_recording, + scheduledRecording.getProgramDisplayTitle(mContext)), + Toast.LENGTH_LONG) + .show(); + } } else { Utils.setRecordingFailedReason(mContext.getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), + mScheduledRecording.getProgramDisplayTitle(mContext)); } } }); diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java index cd79a631..d958c4a1 100644 --- a/src/com/android/tv/dvr/ScheduledProgramReaper.java +++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/recorder/Scheduler.java index ce78e1be..19e73342 100644 --- a/src/com/android/tv/dvr/Scheduler.java +++ b/src/com/android/tv/dvr/recorder/Scheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.app.AlarmManager; import android.app.PendingIntent; @@ -32,8 +32,12 @@ import android.util.Range; import com.android.tv.InputSessionManager; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java index 5ed12ce8..8a211f66 100644 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -23,7 +23,6 @@ import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -36,9 +35,16 @@ import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesInfo; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; import com.android.tv.experiments.Experiments; import java.util.ArrayList; @@ -52,11 +58,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.Set; /** - * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}. + * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for + * the {@link com.android.tv.dvr.data.SeriesRecording}. * <p> * The current implementation assumes that the series recordings are scheduled only for one channel. */ @@ -85,15 +91,13 @@ public class SeriesRecordingScheduler { private final DvrManager mDvrManager; private final WritableDvrDataManager mDataManager; private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>(); - private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>(); + private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks = + new LongSparseArray<>(); private final Set<String> mFetchedSeriesIds = new ArraySet<>(); private final SharedPreferences mSharedPreferences; private boolean mStarted; private boolean mPaused; private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); - private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners = - new CopyOnWriteArraySet<>(); - private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { @Override @@ -107,7 +111,7 @@ public class SeriesRecordingScheduler { public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { // Cancel the update. for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { + iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR).isEmpty()) { @@ -115,6 +119,13 @@ public class SeriesRecordingScheduler { iter.remove(); } } + for (SeriesRecording seriesRecording : seriesRecordings) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId()); + if (task != null) { + task.cancel(true); + mFetchSeriesInfoTasks.remove(seriesRecording.getId()); + } + } } @Override @@ -226,7 +237,8 @@ public class SeriesRecordingScheduler { } if (DEBUG) Log.d(TAG, "stop"); mStarted = false; - for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { + for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i)); task.cancel(true); } mFetchSeriesInfoTasks.clear(); @@ -250,7 +262,7 @@ public class SeriesRecordingScheduler { if (Experiments.CLOUD_EPG.get()) { FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); task.execute(); - mFetchSeriesInfoTasks.add(task); + mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); } } @@ -363,20 +375,6 @@ public class SeriesRecordingScheduler { } } - /** - * Adds {@link OnSeriesRecordingUpdatedListener}. - */ - public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { - mOnSeriesRecordingUpdatedListeners.add(listener); - } - - /** - * Removes {@link OnSeriesRecordingUpdatedListener}. - */ - public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { - mOnSeriesRecordingUpdatedListeners.remove(listener); - } - private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { @@ -403,8 +401,7 @@ public class SeriesRecordingScheduler { /** * @see #pickOneProgramPerEpisode(List, List) */ - @VisibleForTesting - static LongSparseArray<List<Program>> pickOneProgramPerEpisode( + public static LongSparseArray<List<Program>> pickOneProgramPerEpisode( DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, List<Program> programs) { // Initialize. @@ -415,7 +412,7 @@ public class SeriesRecordingScheduler { seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); } // Group programs by the episode. - Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>(); + Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>(); for (Program program : programs) { long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); if (TextUtils.isEmpty(program.getSeasonNumber()) @@ -424,17 +421,17 @@ public class SeriesRecordingScheduler { result.get(seriesRecordingId).add(program); continue; } - ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId, + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()); - List<Program> programsForEpisode = programsForEpisodeMap.get(episode); + List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); if (programsForEpisode == null) { programsForEpisode = new ArrayList<>(); - programsForEpisodeMap.put(episode, programsForEpisode); + programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); } programsForEpisode.add(program); } // Pick one program. - for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) { + for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) { List<Program> programsForEpisode = entry.getValue(); Collections.sort(programsForEpisode, new Comparator<Program>() { @Override @@ -512,13 +509,6 @@ public class SeriesRecordingScheduler { mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); } } - if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { - for (OnSeriesRecordingUpdatedListener listener - : mOnSeriesRecordingUpdatedListeners) { - listener.onSeriesRecordingUpdated( - SeriesRecording.toArray(getSeriesRecordings())); - } - } } @Override @@ -561,19 +551,12 @@ public class SeriesRecordingScheduler { mFetchedSeriesIds.add(seriesInfo.getId()); updateFetchedSeries(); } - mFetchSeriesInfoTasks.remove(this); + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); } @Override protected void onCancelled(SeriesInfo seriesInfo) { - mFetchSeriesInfoTasks.remove(this); + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); } } - - /** - * A listener to notify when series recording are updated. - */ - public interface OnSeriesRecordingUpdatedListener { - void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); - } } diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java new file mode 100644 index 00000000..ec3b5065 --- /dev/null +++ b/src/com/android/tv/dvr/ui/BigArguments.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.annotation.NonNull; + +import com.android.tv.common.SoftPreconditions; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stores the object to pass through activities/fragments. + */ +public class BigArguments { + private final static String TAG = "BigArguments"; + private static Map<String, Object> sBigArgumentMap = new HashMap<>(); + + /** + * Sets the argument. + */ + public static void setArgument(String name, @NonNull Object value) { + SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null"); + sBigArgumentMap.put(name, value); + } + + /** + * Returns the argument which is associated to the name. + */ + public static Object getArgument(String name) { + return sBigArgumentMap.get(name); + } + + /** + * Resets the arguments. + */ + public static void reset() { + sBigArgumentMap.clear(); + } +} diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java new file mode 100644 index 00000000..cddece73 --- /dev/null +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.transition.ChangeImageTransform; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import com.android.tv.R; + +import java.util.Map; + +/** + * TODO: Remove this class once b/32405620 is fixed. + * This class is for the workaround of b/32405620 and only for the shared element transition between + * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and + * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}. + */ +public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { + private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; + + public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + applyParentScale(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + applyParentScale(transitionValues); + } + + private void applyParentScale(TransitionValues transitionValues) { + View view = transitionValues.view; + Map<String, Object> values = transitionValues.values; + Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX); + if (matrix != null && view.getId() == R.id.details_overview_image + && view instanceof ImageView) { + ImageView imageView = (ImageView) view; + if (imageView.getScaleType() == ScaleType.CENTER_INSIDE + && imageView.getDrawable() instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); + if (bitmap.getWidth() < imageView.getWidth() + && bitmap.getHeight() < imageView.getHeight()) { + float scale = imageView.getContext().getResources().getFraction( + R.fraction.lb_focus_zoom_factor_medium, 1, 1); + matrix.postScale(scale, scale, imageView.getWidth() / 2, + imageView.getHeight() / 2); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java deleted file mode 100644 index 5d8e20ff..00000000 --- a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrManager; - -/** - * {@link RecordingDetailsFragment} for current recording in DVR. - */ -public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_STOP_RECORDING = 1; - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, - res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, - res.getDrawable(R.drawable.lb_ic_stop))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_STOP_RECORDING) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()) - .getDvrManager(); - dvrManager.stopRecording(getRecording()); - } - getActivity().finish(); - } - }; - } -} diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index 9df228d1..936e9c31 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -28,11 +28,9 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.RecordedProgram; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 78f21784..3c73cb47 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -25,15 +25,12 @@ import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; -import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.ScheduledRecording; import java.util.List; diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index 837d8ab2..880dc8ac 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -27,7 +27,7 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index e7be4d0a..5985f56f 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -34,10 +34,9 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ConflictChecker; -import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.recorder.ConflictChecker; +import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java deleted file mode 100644 index 73ddcdd0..00000000 --- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; - -import java.util.List; - -public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; - - @Override - public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_forget_storage_title); - String description = getResources().getString( - R.string.dvr_error_forget_storage_description); - return new Guidance(title, description, null, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() != ACTION_FORGET_STORAGE) { - dismissDialog(); - return; - } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - dvrManager.forgetStorage(mInputId); - Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { - // Since we removed everything, just finish the activity. - activity.finish(); - } else { - dismissDialog(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index d26e6836..433588da 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -16,10 +16,12 @@ package com.android.tv.dvr.ui; +import android.app.Activity; import android.app.DialogFragment; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; @@ -29,11 +31,26 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; + +import java.util.List; public class DvrGuidedStepFragment extends GuidedStepFragment { + /** + * Action ID for "recording/scheduling the program anyway". + */ + public static final int ACTION_RECORD_ANYWAY = 1; + /** + * Action ID for "deleting existed recordings". + */ + public static final int ACTION_DELETE_RECORDINGS = 2; + /** + * Action ID for "cancelling current recording request". + */ + public static final int ACTION_CANCEL_RECORDING = 3; + private DvrManager mDvrManager; private OnActionClickListener mOnActionClickListener; @@ -86,4 +103,35 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { protected void setOnActionClickListener(OnActionClickListener listener) { mOnActionClickListener = listener; } + + /** + * The inner guided step fragment for + * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * .DvrNoFreeSpaceErrorDialogFragment}. + */ + public static class DvrNoFreeSpaceErrorFragment + extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title), + getString(R.string.dvr_error_no_free_space_description), null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_DELETE_RECORDINGS) + .title(R.string.dvr_action_delete_recordings) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_CANCEL_RECORDING) + .title(R.string.dvr_action_record_cancel) + .build()); + } + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 2b132db8..9054dd03 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dvr.DvrStorageStatusManager; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; @@ -166,6 +167,17 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } /** + * A dialog fragment to show error message when there is no enough free space to record. + */ + public static class DvrNoFreeSpaceErrorDialogFragment + extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment(); + } + } + + /** * A dialog fragment to show error message when the current storage is too small to * support DVR */ diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java index 3b1dbfa0..3c5df1a6 100644 --- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -24,19 +25,67 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import java.util.ArrayList; import java.util.List; public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_DONE = 1; - private static final int ACTION_OPEN_DVR = 2; + /** + * Key for the failed scheduled recordings information. + */ + public static final String FAILED_SCHEDULED_RECORDING_INFOS = + "failed_scheduled_recording_infos"; + + private static final String TAG = "DvrInsufficientSpaceErrorFragment"; + + private static final int ACTION_VIEW_RECENT_RECORDINGS = 1; + + private ArrayList<String> mFailedScheduledRecordingInfos; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle args = getArguments(); + if (args != null) { + mFailedScheduledRecordingInfos = + args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS); + } + SoftPreconditions.checkState( + mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(), + TAG, "failed scheduled recording is null"); + } @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_insufficient_space_title); - String description = getResources() - .getString(R.string.dvr_error_insufficient_space_description); + String title; + String description; + int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size(); + if (failedScheduledRecordingSize == 1) { + title = getString( + R.string.dvr_error_insufficient_space_title_one_recording, + mFailedScheduledRecordingInfos.get(0)); + description = getString( + R.string.dvr_error_insufficient_space_description_one_recording, + mFailedScheduledRecordingInfos.get(0)); + } else if (failedScheduledRecordingSize == 2) { + title = getString( + R.string.dvr_error_insufficient_space_title_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + description = getString( + R.string.dvr_error_insufficient_space_description_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + } else { + title = getString( + R.string.dvr_error_insufficient_space_title_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + description = getString( + R.string.dvr_error_insufficient_space_description_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + } return new Guidance(title, description, null, null); } @@ -44,26 +93,21 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_DONE) - .title(getResources().getString(R.string.dvr_action_error_done)) + .clickAction(GuidedAction.ACTION_ID_OK) .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext()) - .getDvrDataManager(); - if (!(dvrDataManager.getRecordedPrograms().isEmpty() - && dvrDataManager.getStartedRecordings().isEmpty() - && dvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && dvrDataManager.getSeriesRecordings().isEmpty())) { - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OPEN_DVR) - .title(getResources().getString(R.string.dvr_action_error_open_dvr)) - .build()); + if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) { + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_VIEW_RECENT_RECORDINGS) + .title(getResources().getString( + R.string.dvr_error_insufficient_space_action_view_recent_recordings)) + .build()); } } @Override public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_OPEN_DVR) { - Intent intent = new Intent(getActivity(), DvrActivity.class); + if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) { + Intent intent = new Intent(getActivity(), DvrBrowseActivity.class); getActivity().startActivity(intent); } dismissDialog(); diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index 2e2c2849..8dc9eb4e 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -17,29 +17,27 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; +import android.provider.Settings; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; +import android.util.Log; import com.android.tv.R; -import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; + private static String TAG = "DvrMissingStorageErrorFragment"; + + private static final int ACTION_OK = 1; + private static final int ACTION_OPEN_STORAGE_SETTINGS = 2; @Override public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); super.onCreate(savedInstanceState); } @@ -55,25 +53,31 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) + .id(ACTION_OK) + .title(android.R.string.ok) .build()); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) + .id(ACTION_OPEN_STORAGE_SETTINGS) + .title(getResources().getString(R.string.dvr_action_error_storage_settings)) .build()); } @Override public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_FORGET_STORAGE) { - DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment(); - Bundle args = new Bundle(); - args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId); - fragment.setArguments(args); - GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); + Activity activity = getActivity(); + if (activity instanceof DvrDetailsActivity) { + activity.finish(); + } else { + dismissDialog(); + } + if (action.getId() != ACTION_OPEN_STORAGE_SETTINGS) { return; } - dismissDialog(); + final Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Can't start internal storage settings activity", e); + } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java index 158bd824..562898a3 100644 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java @@ -33,7 +33,7 @@ import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.ArrayList; import java.util.List; @@ -41,7 +41,7 @@ import java.util.List; /** * Fragment for DVR series recording settings. */ -public class PrioritySettingsFragment extends GuidedStepFragment { +public class DvrPrioritySettingsFragment extends GuidedStepFragment { /** * Name of series recording id starting the fragment. * Type: Long @@ -162,7 +162,6 @@ public class PrioritySettingsFragment extends GuidedStepFragment { return; } if (action.getId() < 0) { - int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording); mSelectedRecording = null; for (int i = 0; i < mSeriesRecordings.size(); ++i) { updateItem(i); @@ -248,4 +247,4 @@ public class PrioritySettingsFragment extends GuidedStepFragment { titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index da6d1637..d6008315 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -32,9 +32,8 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; @@ -48,18 +47,26 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { + /** + * Key for the whether to add the current program to series. + * Type: boolean + */ + public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series"; + private static final String TAG = "DvrScheduleFragment"; private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; private Program mProgram; + private boolean mAddCurrentProgramToSeries; @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); + mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, @@ -139,8 +146,10 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { .build(); getDvrManager().updateSeriesRecording(seriesRecording); } + DvrUiHelper.startSeriesSettingsActivity(getContext(), - seriesRecording.getId(), null, true, true, true); + seriesRecording.getId(), null, true, true, true, + mAddCurrentProgramToSeries ? mProgram : null); dismissDialog(); } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index f57e4b05..667af34a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -22,9 +22,6 @@ import android.support.v17.leanback.app.GuidedStepFragment; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ui.SeriesDeletionFragment; -import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -42,7 +39,7 @@ public class DvrSeriesDeletionActivity extends Activity { setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. if (savedInstanceState == null) { - SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment(); + DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java index 36e3cfc1..8bf8560f 100644 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -33,9 +33,10 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -47,7 +48,7 @@ import java.util.concurrent.TimeUnit; /** * Fragment for DVR series recording settings. */ -public class SeriesDeletionFragment extends GuidedStepFragment { +public class DvrSeriesDeletionFragment extends GuidedStepFragment { private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, @@ -218,8 +219,8 @@ public class SeriesDeletionFragment extends GuidedStepFragment { private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)), - TimeUnit.MILLISECONDS.toMinutes(durationMs)); + Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), + Utils.getRoundOffMinsFromMs(durationMs)); } else { return getResources().getString(R.string.dvr_series_watched_info_seconds, Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index 1173df46..8f880f16 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -25,22 +25,29 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.data.Program; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.util.List; public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { + /** + * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. + * Type: List<{@link Program}> + */ + public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs"; + private final static long SERIES_RECORDING_ID_NOT_SET = -1; private final static int ACTION_VIEW_SCHEDULES = 1; - private DvrScheduleManager mDvrScheduleManager; private SeriesRecording mSeriesRecording; private boolean mShowViewScheduleOption; + private List<Program> mPrograms; private int mSchedulesAddedCount = 0; private boolean mHasConflict = false; @@ -58,22 +65,25 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { } mShowViewScheduleOption = getArguments().getBoolean( DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); - mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() .getSeriesRecording(seriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } + mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); + BigArguments.reset(); mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + DvrScheduleManager dvrScheduleManager = + TvApplication.getSingletons(context).getDvrScheduleManager(); List<ScheduledRecording> conflictingRecordings = - mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); + dvrScheduleManager.getConflictingSchedules(mSeriesRecording); mHasConflict = !conflictingRecordings.isEmpty(); for (ScheduledRecording recording : conflictingRecordings) { if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { ++mInThisSeriesConflictCount; - } else { + } else if (recording.getPriority() < mSeriesRecording.getPriority()) { ++mOutThisSeriesConflictCount; } } @@ -113,6 +123,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { .TYPE_SERIES_SCHEDULE); intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, mSeriesRecording); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); startActivity(intent); } getActivity().finish(); @@ -121,30 +134,30 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { private String getDescription() { if (!mHasConflict) { return getResources().getQuantityString( - R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount, + R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means // mHasConflict is false. So we don't need to check that case. if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_this_and_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_this_and_other_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount + mOutThisSeriesConflictCount); } else if (mInThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_this_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount); } else { if (mOutThisSeriesConflictCount == 1) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_one_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_one_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mOutThisSeriesConflictCount); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 3f7671b3..6dd20b3a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.app.Activity; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; @@ -38,25 +37,34 @@ public class DvrSeriesSettingsActivity extends Activity { /** * Name of the boolean flag to decide if the series recording with empty schedule and recording * will be removed. + * Type: boolean */ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; /** * 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 channel id list. If the channel list is given, we show the channels - * from the values in channel option. - * Type: Long array + * Name of the program list. The list contains the programs which belong to the series. + * Type: List<{@link com.android.tv.data.Program}> */ - public static final String CHANNEL_ID_LIST = "channel_id_list"; + public static final String PROGRAM_LIST = "program_list"; /** * Name of the boolean flag to check if the confirm dialog should show view schedule option. + * Type: boolean */ public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = "show_view_schedule_option_in_dialog"; + /** + * Name of the current program added to series. The current program will be recorded only when + * the series recording is initialized from media controller. But for other case, the current + * program won't be recorded. + */ + public static final String CURRENT_PROGRAM = "current_program"; + @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); @@ -66,7 +74,7 @@ public class DvrSeriesSettingsActivity extends Activity { SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); + DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment(); settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java index 6c05c9c6..f28382da 100644 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java @@ -17,19 +17,13 @@ package com.android.tv.dvr.ui; import android.app.FragmentManager; -import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.util.Log; import android.util.LongSparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ProgressBar; import com.android.tv.R; import com.android.tv.TvApplication; @@ -38,14 +32,14 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecording.ChannelOption; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.data.SeriesRecording.ChannelOption; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; + import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,7 +47,7 @@ import java.util.Set; /** * Fragment for DVR series recording settings. */ -public class SeriesSettingsFragment extends GuidedStepFragment +public class DvrSeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { private static final String TAG = "SeriesSettingsFragment"; private static final boolean DEBUG = false; @@ -66,15 +60,13 @@ public class SeriesSettingsFragment extends GuidedStepFragment private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; private DvrDataManager mDvrDataManager; - private ChannelDataManager mChannelDataManager; - private DvrManager mDvrManager; private SeriesRecording mSeriesRecording; private long mSeriesRecordingId; @ChannelOption int mChannelOption; - private Comparator<Channel> mChannelComparator; private long mSelectedChannelId; private int mBackStackCount; private boolean mShowViewScheduleOptionInDialog; + private Program mCurrentProgram; private String mFragmentTitle; private String mProrityActionTitle; @@ -84,7 +76,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment private String mChannelsActionAllText; private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); private List<Channel> mChannels = new ArrayList<>(); - private EpisodicProgramLoadTask mEpisodicProgramLoadTask; + private List<Program> mPrograms; private GuidedAction mPriorityGuidedAction; private GuidedAction mChannelsGuidedAction; @@ -100,22 +92,24 @@ public class SeriesSettingsFragment extends GuidedStepFragment getActivity().finish(); return; } - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); mShowViewScheduleOptionInDialog = getArguments().getBoolean( DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); + mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); mDvrDataManager.addSeriesRecordingListener(this); - long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - if (channelIds == null) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - collectChannelsInBackground(); - } else { - for (long channelId : channelIds) { - Channel channel = mChannelDataManager.getChannel(channelId); + mPrograms = (List<Program>) BigArguments.getArgument( + DvrSeriesSettingsActivity.PROGRAM_LIST); + BigArguments.reset(); + if (mPrograms == null) { + getActivity().finish(); + return; + } + Set<Long> channelIds = new HashSet<>(); + ChannelDataManager channelDataManager = + TvApplication.getSingletons(context).getChannelDataManager(); + for (Program program : mPrograms) { + long channelId = program.getChannelId(); + if (channelIds.add(channelId)) { + Channel channel = channelDataManager.getChannel(channelId); if (channel != null) { mId2Channel.put(channel.getId(), channel); mChannels.add(channel); @@ -125,16 +119,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment mChannelOption = mSeriesRecording.getChannelOption(); mSelectedChannelId = Channel.INVALID_ID; if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); + Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); if (channel != null) { mSelectedChannelId = channel.getId(); } else { mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; } } - mChannelComparator = new Channel.DefaultComparator(context, - TvApplication.getSingletons(context).getTvInputManagerHelper()); - mChannels.sort(mChannelComparator); + mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR); mFragmentTitle = getString(R.string.dvr_series_settings_title); mProrityActionTitle = getString(R.string.dvr_series_settings_priority); mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); @@ -144,23 +136,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment } @Override + public void onResume() { + super.onResume(); + // To avoid the order of series's priority has changed, but series doesn't get update. + updatePriorityGuidedAction(); + } + + @Override public void onDetach() { super.onDetach(); mDvrDataManager.removeSeriesRecordingListener(this); - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } } @Override public void onDestroy() { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount - && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) - && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { - dvrManager.removeSeriesRecording(mSeriesRecordingId); + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); } super.onDestroy(); } @@ -178,7 +170,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment .id(ACTION_ID_PRIORITY) .title(mProrityActionTitle) .build(); - updatePriorityGuidedAction(false); actions.add(mPriorityGuidedAction); mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) @@ -204,10 +195,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == GuidedAction.ACTION_ID_OK) { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } if (mChannelOption != mSeriesRecording.getChannelOption() || mSeriesRecording.isStopped() || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE @@ -218,28 +205,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment if (mSelectedChannelId != Channel.INVALID_ID) { builder.setChannelId(mSelectedChannelId); } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(builder.build()); - SeriesRecordingScheduler scheduler = - SeriesRecordingScheduler.getInstance(getContext()); - // Since dialog is used even after the fragment is closed, we should - // use application context. - ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( - R.string.dvr_series_schedules_progress_message_updating_programs)); - scheduler.addOnSeriesRecordingUpdatedListener( - new OnSeriesRecordingUpdatedListener() { - @Override - public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - dialog.dismiss(); - scheduler.removeOnSeriesRecordingUpdatedListener(this); - showConfirmDialog(); - return; - } - } - } - }); + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + dvrManager.updateSeriesRecording(builder.build()); + if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL + || mSelectedChannelId == mCurrentProgram.getChannelId())) { + dvrManager.addSchedule(mCurrentProgram); + } + updateSchedulesToSeries(); + showConfirmDialog(); } else { showConfirmDialog(); } @@ -247,9 +220,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment finishGuidedStepFragments(); } else if (actionId == ACTION_ID_PRIORITY) { FragmentManager fragmentManager = getFragmentManager(); - PrioritySettingsFragment fragment = new PrioritySettingsFragment(); + DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); Bundle args = new Bundle(); - args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, mSeriesRecording.getId()); fragment.setArguments(args); GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); @@ -281,7 +254,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment private void updateChannelsGuidedAction(boolean notifyActionChanged) { if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else { + } else if (mId2Channel.get(mSelectedChannelId) != null){ mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) .getDisplayText()); } @@ -290,7 +263,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment } } - private void updatePriorityGuidedAction(boolean notifyActionChanged) { + private void updatePriorityGuidedAction() { int totalSeriesCount = 0; int priorityOrder = 0; for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { @@ -312,49 +285,38 @@ public class SeriesSettingsFragment extends GuidedStepFragment mPriorityGuidedAction.setDescription(getString( R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); - } + notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); } - private void collectChannelsInBackground() { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); + private void updateSchedulesToSeries() { + List<Program> recordingCandidates = new ArrayList<>(); + Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>(); + for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { + if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + scheduledEpisodes.add(new SeasonEpisodeNumber( + r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber())); + } } - mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { - @Override - protected void onPostExecute(List<Program> programs) { - mEpisodicProgramLoadTask = null; - Set<Long> channelIds = new HashSet<>(); - for (Program program : programs) { - channelIds.add(program.getChannelId()); - } - boolean channelAdded = false; - for (Long channelId : channelIds) { - if (mId2Channel.get(channelId) != null) { - continue; - } - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - channelAdded = true; - mId2Channel.put(channelId, channel); - mChannels.add(channel); - if (DEBUG) Log.d(TAG, "Added channel: " + channel); - } - } - if (!channelAdded) { - return; - } - mChannels.sort(mChannelComparator); - mChannelsGuidedAction.setSubActions(buildChannelSubAction()); - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); + for (Program program : mPrograms) { + // Removes current programs and scheduled episodes out, matches the channel option. + if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() + && mSeriesRecording.matchProgram(program) + && !scheduledEpisodes.contains(new SeasonEpisodeNumber( + mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) { + recordingCandidates.add(program); } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - mEpisodicProgramLoadTask.execute(); + } + if (recordingCandidates.isEmpty()) { + return; + } + List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) + .get(mSeriesRecordingId); + if (!programsToSchedule.isEmpty()) { + TvApplication.getSingletons(getContext()).getDvrManager() + .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); + } } private List<GuidedAction> buildChannelSubAction() { @@ -373,8 +335,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment } private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity( - getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); + DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, + mShowViewScheduleOptionInDialog, mPrograms); finishGuidedStepFragments(); } @@ -382,16 +344,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeriesRecording.getId()) { + finishGuidedStepFragments(); + return; + } + } + } @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { if (seriesRecording.getId() == mSeriesRecordingId) { mSeriesRecording = seriesRecording; - updatePriorityGuidedAction(true); + updatePriorityGuidedAction(); return; } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index c3867886..b476fff7 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -33,7 +33,7 @@ import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -131,15 +131,8 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_stop_recording_dialog_title); String description; if (mStopReason == REASON_ON_CONFLICT) { - String programTitle = mSchedule.getProgramTitle(); - if (TextUtils.isEmpty(programTitle)) { - ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); - Channel channel = channelDataManager.getChannel(mSchedule.getChannelId()); - programTitle = channel.getDisplayName(); - } description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, - mSchedule.getProgramTitle()); + mSchedule.getProgramDisplayTitle(getContext())); } else { description = getString(R.string.dvr_stop_recording_dialog_description); } diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java index feaa2357..fe3a4a60 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -31,8 +31,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java index c0d3b0c5..507db6e7 100644 --- a/src/com/android/tv/dvr/DvrUiHelper.java +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui; import android.annotation.TargetApi; import android.app.Activity; +import android.app.ProgressDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.media.tv.TvInputManager; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityOptionsCompat; -import android.text.TextUtils; import android.widget.ImageView; import android.widget.Toast; @@ -36,32 +38,36 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ui.DvrDetailsActivity; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrStorageStatusManager; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrSchedulesActivity; -import com.android.tv.dvr.ui.DvrSeriesDeletionActivity; -import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity; -import com.android.tv.dvr.ui.DvrSeriesSettingsActivity; -import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; +import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; /** * A helper class for DVR UI. @@ -69,94 +75,54 @@ import java.util.List; @MainThread @TargetApi(Build.VERSION_CODES.N) public class DvrUiHelper { - /** - * Handles the action to create the new schedule. It returns {@code true} if the schedule is - * added and there's no additional UI, otherwise {@code false}. - */ - public static boolean handleCreateSchedule(MainActivity activity, Program program) { - if (program == null) { - return false; - } - DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); - if (!program.isEpisodic()) { - // One time recording. - dvrManager.addSchedule(program); - if (!dvrManager.getConflictingSchedules(program).isEmpty()) { - DvrUiHelper.showScheduleConflictDialog(activity, program); - return false; - } - } else { - SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null || seriesRecording.isStopped()) { - DvrUiHelper.showScheduleDialog(activity, program); - return false; - } else { - // Show recorded program rather than the schedule. - RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (recordedProgram != null) { - DvrUiHelper.showAlreadyRecordedDialog(activity, program); - return false; - } - ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (duplicate != null - && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || duplicate.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - DvrUiHelper.showAlreadyScheduleDialog(activity, program); - return false; - } - // Just add the schedule. - dvrManager.addSchedule(program); - } - } - return true; + private static String TAG = "DvrUiHelper"; - } + private static ProgressDialog sProgressDialog = null; /** * Checks if the storage status is good for recording and shows error messages if needed. * - * @return true if the storage status is fine to be recorded for {@code inputId}. + * @param recordingRequestRunnable if the storage status is OK to record or users choose to + * perform the operation anyway, this Runnable will run. */ - public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) { - if (!Utils.isBundledInput(inputId)) { - return true; - } - DvrStorageStatusManager dvrStorageStatusManager = - TvApplication.getSingletons(activity).getDvrStorageStatusManager(); - int status = dvrStorageStatusManager.getDvrStorageStatus(); - if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { - showDvrSmallSizedStorageErrorDialog(activity); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { - showDvrMissingStorageErrorDialog(activity, inputId); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) { - // TODO: handle insufficient storage case. - return true; - } else { - return true; + public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId, + Runnable recordingRequestRunnable) { + if (Utils.isBundledInput(inputId)) { + switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager() + .getDvrStorageStatus()) { + case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: + showDvrSmallSizedStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_MISSING: + showDvrMissingStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: + showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable); + return; + } } + recordingRequestRunnable.run(); } /** * Shows the schedule dialog. */ - public static void showScheduleDialog(MainActivity activity, Program program) { + public static void showScheduleDialog(Activity activity, Program program, + boolean addCurrentProgramToSeries) { if (SoftPreconditions.checkNotNull(program) == null) { return; } Bundle args = new Bundle(); args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, + addCurrentProgramToSeries); showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); } /** * Shows the recording duration options dialog. */ - public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) { + public static void showChannelRecordDurationOptions(Activity activity, Channel channel) { if (SoftPreconditions.checkNotNull(channel) == null) { return; } @@ -168,7 +134,7 @@ public class DvrUiHelper { /** * Shows the dialog which says that the new schedule conflicts with others. */ - public static void showScheduleConflictDialog(MainActivity activity, Program program) { + public static void showScheduleConflictDialog(Activity activity, Program program) { if (program == null) { return; } @@ -192,20 +158,47 @@ public class DvrUiHelper { /** * Shows DVR insufficient space error dialog. */ - public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) { - showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null); + public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, + Set<String> failedScheduledRecordingInfoSet) { + Bundle args = new Bundle(); + ArrayList<String> failedScheduledRecordingInfoArray = + new ArrayList<>(failedScheduledRecordingInfoSet); + args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, + failedScheduledRecordingInfoArray); + showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args); Utils.clearRecordingFailedReason(activity, TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.clearFailedScheduledRecordingInfoSet(activity); + } + + /** + * Shows DVR no free space error dialog. + * + * @param recordingRequestRunnable the recording request to be executed when users choose + * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. + */ + public static void showDvrNoFreeSpaceErrorDialog(Activity activity, + Runnable recordingRequestRunnable) { + DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment(); + fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { + recordingRequestRunnable.run(); + } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { + Intent intent = new Intent(activity, DvrBrowseActivity.class); + activity.startActivity(intent); + } + } + }); + showDialogFragment(activity, fragment, null); } /** * Shows DVR missing storage error dialog. */ - private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { - SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId)); - Bundle args = new Bundle(); - args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId); - showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args); + private static void showDvrMissingStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); } /** @@ -231,7 +224,7 @@ public class DvrUiHelper { /** * Shows "already scheduled" dialog. */ - public static void showAlreadyScheduleDialog(MainActivity activity, Program program) { + public static void showAlreadyScheduleDialog(Activity activity, Program program) { if (program == null) { return; } @@ -243,7 +236,7 @@ public class DvrUiHelper { /** * Shows "already recorded" dialog. */ - public static void showAlreadyRecordedDialog(MainActivity activity, Program program) { + public static void showAlreadyRecordedDialog(Activity activity, Program program) { if (program == null) { return; } @@ -252,6 +245,87 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); } + /** + * Handle the request of recording a current program. It will handle creating schedules and + * shows the proper dialog and toast message respectively for timed-recording and program + * recording cases. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingCurrentProgram(Activity activity, + Channel channel, Program program, boolean addProgramToSeries) { + if (program == null) { + DvrUiHelper.showChannelRecordDurationOptions(activity, channel); + } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString(R.string.dvr_msg_current_program_scheduled, + program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Handle the request of recording a future program. It will handle creating schedules and + * shows the proper toast message. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingFutureProgram(Activity activity, + Program program, boolean addProgramToSeries) { + if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString( + R.string.dvr_msg_program_scheduled, program.getTitle()); + ToastUtils.show(activity, msg, Toast.LENGTH_SHORT); + } + } + + /** + * Handles the action to create the new schedule. It returns {@code true} if the schedule is + * added and there's no additional UI, otherwise {@code false}. + */ + private static boolean handleCreateSchedule(Activity activity, Program program, + boolean addProgramToSeries) { + if (program == null) { + return false; + } + DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); + if (!program.isEpisodic()) { + // One time recording. + dvrManager.addSchedule(program); + if (!dvrManager.getConflictingSchedules(program).isEmpty()) { + DvrUiHelper.showScheduleConflictDialog(activity, program); + return false; + } + } else { + // Show recorded program rather than the schedule. + RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (recordedProgram != null) { + DvrUiHelper.showAlreadyRecordedDialog(activity, program); + return false; + } + ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (duplicate != null + && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || duplicate.getState() + == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + DvrUiHelper.showAlreadyScheduleDialog(activity, program); + return false; + } + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); + if (seriesRecording == null || seriesRecording.isStopped()) { + DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries); + return false; + } else { + // Just add the schedule. + dvrManager.addSchedule(program); + } + } + return true; + } + private static void showDialogFragment(Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args) { showDialogFragment(activity, dialogFragment, args, false, false); @@ -341,19 +415,66 @@ public class DvrUiHelper { /** * Shows the series settings activity. * - * @param channelIds Channel ID list which has programs belonging to the series. + * @param programs list of programs which belong to the series. */ public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, - @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { + @Nullable List<Program> programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager() + .getSeriesRecording(seriesRecordingId); + if (series == null) { + return; + } + if (programs != null) { + startSeriesSettingsActivityInternal(context, seriesRecordingId, programs, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } else { + EpisodicProgramLoadTask episodicProgramLoadTask = + new EpisodicProgramLoadTask(context, series) { + @Override + protected void onPostExecute(List<Program> loadedPrograms) { + sProgressDialog.dismiss(); + sProgressDialog = null; + startSeriesSettingsActivityInternal(context, seriesRecordingId, + loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + sProgressDialog = ProgressDialog.show(context, null, context.getString( + R.string.dvr_series_progress_message_reading_programs), true, true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + episodicProgramLoadTask.cancel(true); + sProgressDialog = null; + } + }); + episodicProgramLoadTask.execute(); + } + } + + private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId, + @NonNull List<Program> programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SoftPreconditions.checkState(programs != null, + TAG, "Start series settings activity but programs is null"); Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); - intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs); intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, removeEmptySeriesSchedule); intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, showViewScheduleOptionInDialog); + intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram); context.startActivity(intent); } @@ -361,7 +482,8 @@ public class DvrUiHelper { * Shows "series recording scheduled" dialog activity. */ public static void StartSeriesScheduledDialogActivity(Context context, - SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, + List<Program> programs) { if (seriesRecording == null) { return; } @@ -370,6 +492,9 @@ public class DvrUiHelper { seriesRecording.getId()); intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, showViewScheduleOptionInDialog); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, + programs); context.startActivity(intent); } @@ -447,4 +572,4 @@ public class DvrUiHelper { Utils.toTimeString(endTimeMs, false)); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java new file mode 100644 index 00000000..4f06ebcf --- /dev/null +++ b/src/com/android/tv/dvr/ui/FadeBackground.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** + * This transition fades in/out of the background of the view by changing the background color. + */ +public class FadeBackground extends Transition { + private final int mMode; + + public FadeBackground(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadeBackground); + mMode = a.getInt(R.styleable.FadeBackground_fadingMode, Visibility.MODE_IN); + a.recycle(); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { } + + @Override + public void captureEndValues(TransitionValues transitionValues) { } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + Drawable background = endValues.view.getBackground(); + if (background instanceof ColorDrawable) { + int color = ((ColorDrawable) background).getColor(); + int transparentColor = Color.argb(0, Color.red(color), Color.green(color), + Color.blue(color)); + return mMode == Visibility.MODE_OUT + ? ObjectAnimator.ofArgb(background, "color", transparentColor) + : ObjectAnimator.ofArgb(background, "color", transparentColor, color); + } + return null; + } +} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java deleted file mode 100644 index d320816e..00000000 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Handler; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dialog.SafeDismissDialogFragment; - -import java.util.concurrent.TimeUnit; - -public class HalfSizedDialogFragment extends SafeDismissDialogFragment { - public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); - public static final String TRACKER_LABEL = "Half sized dialog"; - - private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); - - private OnActionClickListener mOnActionClickListener; - - private Handler mHandler = new Handler(); - private Runnable mAutoDismisser = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.halfsized_dialog, container, false); - } - - @Override - public void onStart() { - super.onStart(); - getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - } - - @Override - public void onPause() { - super.onPause(); - if (mOnActionClickListener != null) { - // Dismisses the dialog to prevent the callback being forgotten during - // fragment re-creating. - dismiss(); - } - } - - @Override - public void onStop() { - super.onStop(); - mHandler.removeCallbacks(mAutoDismisser); - } - - @Override - public int getTheme() { - return R.style.Theme_TV_dialog_HalfSizedDialog; - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - /** - * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog - * will be automatically closed when it's paused to prevent the fragment being re-created by - * the framework, which will result the listener being forgotten. - */ - public void setOnActionClickListener(OnActionClickListener listener) { - mOnActionClickListener = listener; - } - - /** - * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. - */ - protected OnActionClickListener getOnActionClickListener() { - return mOnActionClickListener; - } - - /** - * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments - * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier - * of the action user clicked. - */ - public interface OnActionClickListener { - void onActionClick(long actionId); - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 393a5ff3..8c0af9ed 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -20,11 +20,15 @@ import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PresenterSelector; +import com.android.tv.common.SoftPreconditions; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Keeps a set of items sorted @@ -35,16 +39,18 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { private final Comparator<T> mComparator; private final int mMaxItemCount; private int mExtraItemCount; + private final Set<Long> mIds = new HashSet<>(); - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { this(presenterSelector, comparator, Integer.MAX_VALUE); } - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; + setHasStableIds(true); } /** @@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { final void setInitialItems(List<T> items) { List<T> itemsCopy = new ArrayList<>(items); Collections.sort(itemsCopy, mComparator); - addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size()))); + for (T item : itemsCopy) { + add(item, true); + if (size() == mMaxItemCount) { + break; + } + } } /** @@ -82,6 +93,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * the end to save search time. */ public final void add(T item, boolean insertToEnd) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); int i; if (insertToEnd) { i = findInsertPosition(item); @@ -89,8 +103,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { i = findInsertPositionBinary(item); } super.add(i, item); - if (size() > mMaxItemCount + mExtraItemCount) { - removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount); + if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) { + Object removedItem = get(mMaxItemCount); + remove(removedItem); } } @@ -100,48 +115,97 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * They will be presented in their insertion order. */ public int addExtraItem(T item) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); super.add(item); return ++mExtraItemCount; } + @Override + public boolean remove(Object item) { + return removeWithId((T) item); + } + /** * Removes an item which has the same ID as {@code item}. */ public boolean removeWithId(T item) { - int index = indexWithTypeAndId(item); - return index >= 0 && index < size() && remove(get(index)); + int index = indexWithId(item); + return index >= 0 && index < size() && removeItems(index, 1) == 1; + } + + @Override + public int removeItems(int position, int count) { + int upperBound = Math.min(position + count, size()); + for (int i = position; i < upperBound; i++) { + mIds.remove(getId((T) get(i))); + } + if (upperBound > size() - mExtraItemCount) { + mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position); + } + return super.removeItems(position, count); + } + + @Override + public void replace(int position, Object item) { + boolean wasExtra = position >= size() - mExtraItemCount; + removeItems(position, 1); + if (!wasExtra) { + add(item); + } else { + addExtraItem((T) item); + } + } + + @Override + public void clear() { + mIds.clear(); + super.clear(); } /** - * Change an item in the list. + * Changes an item in the list. * @param item The item to change. */ public final void change(T item) { - int oldIndex = indexWithTypeAndId(item); + int oldIndex = indexWithId(item); if (oldIndex != -1) { T old = (T) get(oldIndex); if (mComparator.compare(old, item) == 0) { replace(oldIndex, item); return; } - removeItems(oldIndex, 1); + remove(old); } add(item); } /** + * Checks whether the item is in the list. + */ + public final boolean contains(T item) { + return indexWithId(item) != -1; + } + + @Override + public long getId(int position) { + return getId((T) get(position)); + } + + /** * Returns the id of the the given {@code item}, which will be used in {@link #change} to * decide if the given item is already existed in the adapter. * * The id must be stable. */ - abstract long getId(T item); + protected abstract long getId(T item); - private int indexWithTypeAndId(T item) { + private int indexWithId(T item) { long id = getId(item); for (int i = 0; i < size() - mExtraItemCount; i++) { T r = (T) get(i); - if (r.getClass() == item.getClass() && getId(r) == id) { + if (getId(r) == id) { return i; } } diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java index 8b8cd5c5..38a78f5d 100644 --- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.graphics.drawable.Drawable; import android.support.v17.leanback.R; @@ -110,11 +110,7 @@ class ActionPresenterSelector extends PresenterSelector { .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); vh.view.setPaddingRelative(padding, 0, padding, 0); } - if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); - } else { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - } + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); CharSequence line1 = action.getLabel1(); CharSequence line2 = action.getLabel2(); @@ -130,7 +126,7 @@ class ActionPresenterSelector extends PresenterSelector { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); vh.view.setPadding(0, 0, 0, 0); vh.mAction = null; } diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java new file mode 100644 index 00000000..c8f6a03f --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.content.res.Resources; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** + * {@link RecordingDetailsFragment} for current recording in DVR. + */ +public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_STOP_RECORDING = 1; + + private DvrDataManager mDvrDataManger; + private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = + new DvrDataManager.ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId() + && schedule.getState() + != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + getActivity().finish(); + return; + } + } + } + }; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, + res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, + res.getDrawable(R.drawable.lb_ic_stop))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_STOP_RECORDING) { + DvrUiHelper.showStopRecordingDialog(getActivity(), + getRecording().getChannelId(), + DvrStopRecordingFragment.REASON_USER_STOP, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + DvrManager dvrManager = + TvApplication.getSingletons(getContext()) + .getDvrManager(); + dvrManager.stopRecording(getRecording()); + getActivity().finish(); + } + } + }); + } + } + }; + } + + @Override + public void onDetach() { + if (mDvrDataManger != null) { + mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener); + } + super.onDetach(); + } +} diff --git a/src/com/android/tv/dvr/ui/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java index 19521fca..b43d1f12 100644 --- a/src/com/android/tv/dvr/ui/DetailsContent.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.media.tv.TvContract; import android.support.annotation.Nullable; @@ -26,7 +26,7 @@ import com.android.tv.data.Channel; /** * A class for details content. */ -public class DetailsContent { +class DetailsContent { /** Constant for invalid time. */ public static final long INVALID_TIME = -1; diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java index 175f05bc..a2e3fe16 100644 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.animation.Animator; @@ -38,13 +38,14 @@ import com.android.tv.util.Utils; /** * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. + * Typically this Presenter will be used in a + * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. * Most codes of this class is originated from * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. * The latter class are re-used to provide a customized version of * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. */ -public class DetailsContentPresenter extends Presenter { +class DetailsContentPresenter extends Presenter { /** * The ViewHolder for the {@link DetailsContentPresenter}. */ @@ -113,6 +114,20 @@ public class DetailsContentPresenter extends Presenter { public ViewHolder(final View view) { super(view); + view.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + // In case predraw listener was removed in detach, make sure + // we have the proper layout. + addPreDrawListener(); + } + + @Override + public void onViewDetachedFromWindow(View v) { + removePreDrawListener(); + } + }); mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); @@ -276,22 +291,6 @@ public class DetailsContentPresenter extends Presenter { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } - @Override - public void onViewAttachedToWindow(Presenter.ViewHolder holder) { - // In case predraw listener was removed in detach, make sure - // we have the proper layout. - ViewHolder vh = (ViewHolder) holder; - vh.addPreDrawListener(); - super.onViewAttachedToWindow(holder); - } - - @Override - public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { - ViewHolder vh = (ViewHolder) holder; - vh.removePreDrawListener(); - super.onViewDetachedFromWindow(holder); - } - private void setTopMargin(View view, int topMargin) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); lp.topMargin = topMargin; diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java index 6714ecd3..82fe9ce3 100644 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.graphics.drawable.BitmapDrawable; @@ -26,7 +26,7 @@ import android.support.v17.leanback.app.BackgroundManager; /** * The Background Helper. */ -public class DetailsViewBackgroundHelper { +class DetailsViewBackgroundHelper { // Background delay serves to avoid kicking off expensive bitmap loading // in case multiple backgrounds are set in quick succession. private static final int SET_BACKGROUND_DELAY_MS = 100; diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java index 45fb1cf1..2b3dcb25 100644 --- a/src/com/android/tv/dvr/ui/DvrActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.os.Bundle; @@ -25,11 +25,11 @@ import com.android.tv.TvApplication; /** * {@link android.app.Activity} for DVR UI. */ -public class DvrActivity extends Activity { +public class DvrBrowseActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.dvr_main); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java index a6dd31d1..803d1017 100644 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -14,10 +14,9 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; -import android.media.tv.TvInputManager.TvInputCallback; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.BrowseFragment; @@ -25,11 +24,11 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.TitleViewAdapter; -import android.text.TextUtils; import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import com.android.tv.ApplicationSingletons; import com.android.tv.R; @@ -42,9 +41,10 @@ import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.SortedArrayAdapter; import java.util.ArrayList; import java.util.Arrays; @@ -79,6 +79,20 @@ public class DvrBrowseFragment extends BrowseFragment implements private ClassPresenterSelector mPresenterSelector; private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>(); private final Handler mHandler = new Handler(); + private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener = + new OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (oldFocus instanceof RecordingCardView) { + ((RecordingCardView) oldFocus).expandTitle(false, true); + } + if (newFocus instanceof RecordingCardView) { + // If the header transition is ongoing, expand cards immediately without + // animation to make a smooth transition. + ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition()); + } + } + }; private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() { @Override @@ -128,7 +142,7 @@ public class DvrBrowseFragment extends BrowseFragment implements public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { if (mScheduleAdapter != null) { for (ScheduledRecording schedule : schedules) { - onScheduledRecordingStatusChanged(schedule); + onScheduledRecordingConflictStatusChanged(schedule); } } } @@ -154,16 +168,12 @@ public class DvrBrowseFragment extends BrowseFragment implements new ScheduledRecordingPresenter(context)) .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter()); + .addClassPresenter(FullScheduleCardHolder.class, + new FullSchedulesCardPresenter(context)); mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); - setupUiElements(); - setupAdapters(); - mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); - prepareEntranceTransition(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } else { + prepareUiElements(); + if (!startBrowseIfDvrInitialized()) { if (!mDvrDataManager.isDvrScheduleLoadFinished()) { mDvrDataManager.addDvrScheduleLoadFinishedListener(this); } @@ -174,6 +184,19 @@ public class DvrBrowseFragment extends BrowseFragment implements } @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + } + + @Override + public void onDestroyView() { + getView().getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + super.onDestroyView(); + } + + @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); mHandler.removeCallbacks(mUpdateRowsRunnable); @@ -195,25 +218,13 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDvrScheduleLoadFinished() { - List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings)); - List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings(); - onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings)); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } + startBrowseIfDvrInitialized(); mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); } @Override public void onRecordedProgramLoadFinished() { - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, true); - } - updateRows(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } + startBrowseIfDvrInitialized(); mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); } @@ -270,6 +281,18 @@ public class DvrBrowseFragment extends BrowseFragment implements } } + private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (needToShowScheduledRecording(schedule)) { + if (mScheduleAdapter.contains(schedule)) { + mScheduleAdapter.change(schedule); + } + } else { + mScheduleAdapter.removeWithId(schedule); + } + } + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); @@ -295,44 +318,53 @@ public class DvrBrowseFragment extends BrowseFragment implements super.showTitle(flags); } - private void setupUiElements() { + private void prepareUiElements() { setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(false); setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); + mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext())); + setAdapter(mRowsAdapter); + prepareEntranceTransition(); } - private void setupAdapters() { - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); - mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new SeriesAdapter(); - for (int i = 0; i < mGenreAdapters.length; i++) { - mGenreAdapters[i] = new RecordedProgramAdapter(); - } - // Schedule Recordings. - List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); - mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); - // Recorded Programs. - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - // Series Recordings. Series recordings should be added after recorded programs, because - // we build series recordings' latest program information while adding recorded programs. - List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); - handleSeriesRecordingsAdded(recordings); - mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - mRecentRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_recent)), mRecentAdapter); - mRowsAdapter.add(new ListRow(new HeaderItem( - getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); - mSeriesRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_series)), mSeriesAdapter); - updateRows(); - mDvrDataManager.addRecordedProgramListener(this); - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addSeriesRecordingListener(this); - setAdapter(mRowsAdapter); + private boolean startBrowseIfDvrInitialized() { + if (mDvrDataManager.isInitialized()) { + // Setup rows + mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); + mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); + mSeriesAdapter = new SeriesAdapter(); + for (int i = 0; i < mGenreAdapters.length; i++) { + mGenreAdapters[i] = new RecordedProgramAdapter(); + } + // Schedule Recordings. + List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); + onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); + mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); + // Recorded Programs. + for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { + handleRecordedProgramAdded(recordedProgram, false); + } + // Series Recordings. Series recordings should be added after recorded programs, because + // we build series recordings' latest program information while adding recorded programs. + List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); + handleSeriesRecordingsAdded(recordings); + mRecentRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_recent)), mRecentAdapter); + mRowsAdapter.add(new ListRow(new HeaderItem( + getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); + mSeriesRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_series)), mSeriesAdapter); + updateRows(); + // Initialize listeners + mDvrDataManager.addRecordedProgramListener(this); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addSeriesRecordingListener(this); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); + startEntranceTransition(); + return true; + } + return false; } private void handleRecordedProgramAdded(RecordedProgram recordedProgram, @@ -589,10 +621,11 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public long getId(Object item) { + // We takes the inverse number for the ID of recorded programs to make the ID stable. if (item instanceof SeriesRecording) { return ((SeriesRecording) item).getId(); } else if (item instanceof RecordedProgram) { - return ((RecordedProgram) item).getId(); + return -((RecordedProgram) item).getId() - 1; } else { return -1; } diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java index 806c775c..30c81e83 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.os.Bundle; diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java index 21f9c4b4..4d3698ef 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; import android.content.Intent; @@ -48,8 +48,8 @@ import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.util.ImageLoader; import com.android.tv.util.ToastUtils; diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java index 339e5d2f..317b6af3 100644 --- a/src/com/android/tv/dvr/ui/DvrItemPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.support.annotation.CallSuper; @@ -22,16 +22,17 @@ import android.support.v17.leanback.widget.Presenter; import android.view.View; import android.view.View.OnClickListener; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; import java.util.HashSet; -import java.util.Iterator; import java.util.Set; /** * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, - * {@link RecordedProgram}, and {@link SeriesRecording}. + * {@link DvrBrowseFragment}. DVR items might include: + * {@link com.android.tv.dvr.data.ScheduledRecording}, + * {@link com.android.tv.dvr.data.RecordedProgram}, and + * {@link com.android.tv.dvr.data.SeriesRecording}. */ public abstract class DvrItemPresenter extends Presenter { private final Set<ViewHolder> mBoundViewHolders = new HashSet<>(); @@ -49,6 +50,8 @@ public abstract class DvrItemPresenter extends Presenter { @CallSuper public void onUnbindViewHolder(ViewHolder viewHolder) { mBoundViewHolders.remove(viewHolder); + viewHolder.view.setTag(null); + viewHolder.view.setOnClickListener(null); } /** diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java new file mode 100644 index 00000000..37a72eaf --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017 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.browse; + +import android.content.Context; +import android.support.v17.leanback.widget.ListRowPresenter; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** A list row presenter to display expand/fold card views list. */ +public class DvrListRowPresenter extends ListRowPresenter { + public DvrListRowPresenter(Context context) { + super(); + setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + setExpandedRowHeight( + context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height)); + } +} diff --git a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java index d4d4d8ab..311137a9 100644 --- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java +++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; /** * Special object for schedule preview; diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java index 7dd85f45..6d4763d4 100644 --- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; -import android.support.v17.leanback.widget.Presenter; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; import java.util.Collections; @@ -33,23 +33,31 @@ import java.util.List; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -public class FullSchedulesCardPresenter extends Presenter { +class FullSchedulesCardPresenter extends DvrItemPresenter { + private Context mContext; + private final Drawable mIconDrawable; + private final String mCardTitleText; + + public FullSchedulesCardPresenter(Context context) { + mContext = context; + mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule); + mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title); + } + @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { Context context = parent.getContext(); RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view); + return new ViewHolder(view); } @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); + public void onBindViewHolder(ViewHolder vh, Object o) { + final RecordingCardView cardView = (RecordingCardView) vh.view; - cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule)); - cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title)); - List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context) + cardView.setImage(mIconDrawable); + cardView.setTitle(mCardTitleText); + List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext) .getDvrDataManager().getAvailableScheduledRecordings(); int fullDays = 0; if (!scheduledRecordings.isEmpty()) { @@ -57,28 +65,24 @@ public class FullSchedulesCardPresenter extends Presenter { Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) .getStartTimeMs()) + 1; } - cardView.setContent(context.getResources().getQuantityString( + cardView.setContent(mContext.getResources().getQuantityString( R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); - - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - DvrUiHelper.startSchedulesActivity(context, null); - } - }; - baseHolder.view.setOnClickListener(clickListener); + super.onBindViewHolder(vh, o); } @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.reset(); + public void onUnbindViewHolder(ViewHolder vh) { + ((RecordingCardView) vh.view).reset(); + super.onUnbindViewHolder(vh); } - private static final class ScheduledRecordingViewHolder extends ViewHolder { - ScheduledRecordingViewHolder(RecordingCardView view) { - super(view); - } + @Override + protected View.OnClickListener onCreateOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View view) { + DvrUiHelper.startSchedulesActivity(mContext, null); + } + }; } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java index e698b8a2..fe9b9de5 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.media.tv.TvInputManager; @@ -30,10 +30,10 @@ import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; /** - * {@link DetailsFragment} for recorded program in DVR. + * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment implements DvrDataManager.RecordedProgramListener { diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java index 1bf34310..ee978797 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java @@ -14,31 +14,26 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; -import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; -import android.net.Uri; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; -import android.view.View; import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.Utils; -import java.util.concurrent.TimeUnit; - /** * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. */ @@ -50,6 +45,7 @@ public class RecordedProgramPresenter extends DvrItemPresenter { private String mYesterdayString; private final int mProgressBarColor; private final boolean mShowEpisodeTitle; + private final boolean mExpandTitleWhenFocused; private static final class RecordedProgramViewHolder extends ViewHolder implements WatchedPositionChangedListener { @@ -79,25 +75,27 @@ public class RecordedProgramPresenter extends DvrItemPresenter { } } - public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) { + public RecordedProgramPresenter(Context context, boolean showEpisodeTitle, + boolean expandTitleWhenFocused) { mContext = context; - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - mTodayString = context.getString(R.string.dvr_date_today); - mYesterdayString = context.getString(R.string.dvr_date_yesterday); + mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); + mTodayString = mContext.getString(R.string.dvr_date_today); + mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mProgressBarColor = context.getResources() + TvApplication.getSingletons(mContext).getDvrWatchedPositionManager(); + mProgressBarColor = mContext.getResources() .getColor(R.color.play_controls_progress_bar_watched); mShowEpisodeTitle = showEpisodeTitle; + mExpandTitleWhenFocused = expandTitleWhenFocused; } public RecordedProgramPresenter(Context context) { - this(context, false); + this(context, false, false); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext); + RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused); return new RecordedProgramViewHolder(view, mProgressBarColor); } @@ -132,8 +130,7 @@ public class RecordedProgramPresenter extends DvrItemPresenter { isChannelLogo = true; } cardView.setImageUri(imageUri, isChannelLogo); - int durationMinutes = - Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis())); + int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); String durationString = getContext().getResources().getQuantityString( R.plurals.dvr_program_duration, durationMinutes, durationMinutes); cardView.setContent(getDescription(program), durationString); diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java index 51c3b03b..7b0a8cb9 100644 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -14,51 +14,69 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.support.annotation.Nullable; import android.support.v17.leanback.widget.BaseCardView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.ui.ViewUtils; import com.android.tv.util.ImageLoader; /** - * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or - * {@link RecordedProgram} or - * {@link com.android.tv.dvr.SeriesRecording}. + * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} + * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. */ -class RecordingCardView extends BaseCardView { +public class RecordingCardView extends BaseCardView { + // This value should be the same with + // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS + private final static int ANIMATION_DURATION = 150; private final ImageView mImageView; private final int mImageWidth; private final int mImageHeight; private String mImageUri; - private final TextView mTitleView; private final TextView mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; private final View mAffiliatedIconContainer; private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; + private final FrameLayout mTitleArea; + private final TextView mFoldedTitleView; + private final TextView mExpandedTitleView; + private final ValueAnimator mExpandTitleAnimator; + private final int mFoldedTitleHeight; + private final int mExpandedTitleHeight; + private final boolean mExpandTitleWhenFocused; + private boolean mExpanded; - RecordingCardView(Context context) { - this(context, - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width), - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height)); + public RecordingCardView(Context context) { + this(context, false); } - RecordingCardView(Context context, int imageWidth, int imageHeight) { + public RecordingCardView(Context context, boolean expandTitleWhenFocused) { + this(context, context.getResources().getDimensionPixelSize( + R.dimen.dvr_library_card_image_layout_width), context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), + expandTitleWhenFocused); + } + + public RecordingCardView(Context context, int imageWidth, int imageHeight, + boolean expandTitleWhenFocused) { super(context); //TODO(dvr): move these to the layout XML. setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); @@ -75,13 +93,73 @@ class RecordingCardView extends BaseCardView { mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); - mTitleView = (TextView) findViewById(R.id.title); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); + mTitleArea = (FrameLayout) findViewById(R.id.title_area); + mFoldedTitleView = (TextView) findViewById(R.id.title_one_line); + mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines); + mFoldedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); + mExpandedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); + mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION); + mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float value = (Float) valueAnimator.getAnimatedValue(); + mExpandedTitleView.setAlpha(value); + mFoldedTitleView.setAlpha(1.0f - value); + ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight + + (mExpandedTitleHeight - mFoldedTitleHeight) * value)); + } + }); + mExpandTitleWhenFocused = expandTitleWhenFocused; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + if (mExpandTitleWhenFocused) { + if (gainFocus) { + expandTitle(true, true); + } else { + expandTitle(false, true); + } + } + } + + /** + * Expands/folds the title area to show program title with two/one lines. + * + * @param expand {@code true} to expand the title area, or {@code false} to fold it. + * @param withAnimation {@code true} to expand/fold with animation. + */ + public void expandTitle(boolean expand, boolean withAnimation) { + if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) { + if (withAnimation) { + if (expand) { + mExpandTitleAnimator.start(); + } else { + mExpandTitleAnimator.reverse(); + } + } else { + if (expand) { + mFoldedTitleView.setAlpha(0.0f); + mExpandedTitleView.setAlpha(1.0f); + ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight); + } else { + mFoldedTitleView.setAlpha(1.0f); + mExpandedTitleView.setAlpha(0.0f); + ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight); + } + } + mExpanded = expand; + } } void setTitle(CharSequence title) { - mTitleView.setText(title); + mFoldedTitleView.setText(title); + mExpandedTitleView.setText(title); } void setContent(CharSequence majorContent, CharSequence minorContent) { @@ -178,8 +256,9 @@ class RecordingCardView extends BaseCardView { } public void reset() { - mTitleView.setText(null); + mFoldedTitleView.setText(null); + mExpandedTitleView.setText(null); setContent(null, null); mImageView.setImageDrawable(mDefaultImage); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java index 4e19ec3f..a877e05f 100644 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; @@ -26,7 +26,7 @@ import android.text.style.TextAppearanceSpan; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * {@link DetailsFragment} for recordings in DVR. diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java index 60816bb5..eb0f4f0d 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.os.Bundle; @@ -26,7 +26,7 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; /** * {@link RecordingDetailsFragment} for scheduled recording in DVR. diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java index 5f447f13..efc8785a 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; -import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.os.Handler; @@ -32,7 +31,7 @@ import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.concurrent.TimeUnit; @@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -public class ScheduledRecordingPresenter extends DvrItemPresenter { +class ScheduledRecordingPresenter extends DvrItemPresenter { private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); private final ChannelDataManager mChannelDataManager; @@ -92,18 +91,17 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter { } public ScheduledRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); + mContext = context; + ApplicationSingletons singletons = TvApplication.getSingletons(mContext); mChannelDataManager = singletons.getChannelDataManager(); mDvrManager = singletons.getDvrManager(); - mContext = context; - mProgressBarColor = context.getResources() + mProgressBarColor = mContext.getResources() .getColor(R.color.play_controls_recording_icon_color_on_focus); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); + RecordingCardView view = new RecordingCardView(mContext); return new ScheduledRecordingViewHolder(view, mProgressBarColor); } @@ -144,9 +142,8 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter { public void onUnbindViewHolder(ViewHolder baseHolder) { ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; viewHolder.stopUpdateProgressBar(); - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.mScheduledRecording = null; - cardView.reset(); + ((RecordingCardView) viewHolder.view).reset(); super.onUnbindViewHolder(viewHolder); } @@ -174,4 +171,4 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter { cardView.setTitle(title); cardView.setImageUri(imageUri, isChannelLogo); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java index e9e391d4..f7b60b50 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -28,7 +28,6 @@ import android.support.v17.leanback.widget.DetailsOverviewRow; import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; @@ -39,12 +38,11 @@ import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.dvr.ui.SortedArrayAdapter; import java.util.Collections; import java.util.Comparator; @@ -85,7 +83,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mWatchLabel = getString(R.string.dvr_detail_watch); mResumeLabel = getString(R.string.dvr_detail_series_resume); mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); - mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); super.onCreate(savedInstanceState); } @@ -158,7 +156,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); + presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); return presenterSelector; } @@ -203,10 +201,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mDvrDataManager.removeSeriesRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); if (mSeries != null) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { - dvrManager.removeSeriesRecording(mSeries.getId()); - } + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); } mRecordedProgramPresenter.unbindAllViewHolders(); } @@ -265,7 +260,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecording series : seriesRecordings) { if (series.getId() == mSeries.getId()) { - mSeries = null; getActivity().finish(); return; } @@ -372,4 +366,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return program.getId(); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java index c2c0f596..af6ecc19 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; -import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; @@ -34,16 +33,16 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.List; /** * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ -public class SeriesRecordingPresenter extends DvrItemPresenter { +class SeriesRecordingPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrDataManager mDvrDataManager; private final DvrManager mDvrManager; diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index d28f026c..5abd52a1 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -29,7 +29,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * A base fragment to show the list of schedule recordings. diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java index f6e6ac26..a0410bb3 100644 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.list; import android.app.Activity; import android.app.ProgressDialog; @@ -24,15 +24,13 @@ import android.support.annotation.IntDef; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.ui.list.DvrSchedulesFragment; -import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; +import com.android.tv.dvr.ui.BigArguments; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -72,33 +70,47 @@ public class DvrSchedulesActivity extends Activity { getFragmentManager().beginTransaction().add( R.id.fragment_container, schedulesFragment).commit(); } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_schedules_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); - // To get programs faster, hold the update of the series schedules. - SeriesRecordingScheduler.getInstance(this).pauseUpdate(); - new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { - @Override - protected void onPostExecute(List<Program> programs) { - SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); - dialog.dismiss(); - Bundle args = getIntent().getExtras(); - args.putParcelableArrayList(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); - DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true) - .execute(); + if (BigArguments.getArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) { + // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need + // to reset the BigArguments. + showDvrSeriesSchedulesFragment(getIntent().getExtras()); + } else { + final ProgressDialog dialog = ProgressDialog.show(this, null, getString( + R.string.dvr_series_progress_message_reading_programs)); + SeriesRecording seriesRecording = getIntent().getExtras() + .getParcelable(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + // To get programs faster, hold the update of the series schedules. + SeriesRecordingScheduler.getInstance(this).pauseUpdate(); + new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { + @Override + protected void onPostExecute(List<Program> programs) { + SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this) + .resumeUpdate(); + dialog.dismiss(); + Bundle args = getIntent().getExtras(); + BigArguments.reset(); + BigArguments.setArgument( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, + programs == null ? Collections.EMPTY_LIST : programs); + showDvrSeriesSchedulesFragment(args); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); + } } else { finish(); } } + + private void showDvrSeriesSchedulesFragment(Bundle args) { + DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); + schedulesFragment.setArguments(args); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } } diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index 722c9b6e..3cbb500a 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index 42a1e72b..57e7a88f 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui.list; import android.annotation.TargetApi; +import android.content.Context; import android.database.ContentObserver; import android.media.tv.TvContract.Programs; import android.net.Uri; @@ -35,11 +36,13 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.ui.BigArguments; +import java.util.Collections; import java.util.List; /** @@ -47,20 +50,22 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { - private static final String TAG = "DvrSeriesSchedulesFragment"; /** * The key for series recording whose scheduled recording list will be displayed. + * Type: {@link SeriesRecording} */ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; /** - * The key for programs belong to the series recording whose scheduled recording - * list will be displayed. + * The key for programs which belong to the series recording whose scheduled recording list + * will be displayed. + * Type: List<{@link Program}> */ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = "series_schedules_key_series_programs"; private ChannelDataManager mChannelDataManager; + private DvrDataManager mDvrDataManager; private SeriesRecording mSeriesRecording; private List<Program> mPrograms; private EpisodicProgramLoadTask mProgramLoadTask; @@ -87,20 +92,22 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { ((SeriesScheduleRowAdapter) getRowsAdapter()) .onSeriesRecordingUpdated(r); + mSeriesRecording = r; + updateEmptyMessage(); return; } } } }; - private final ContentObserver mContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - executeProgramLoadingTask(); - } - }; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final ContentObserver mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { @Override @@ -120,17 +127,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { } @Override - public void onCreate(Bundle savedInstanceState) { + public void onAttach(Context context) { + super.onAttach(context); Bundle args = getArguments(); if (args != null) { mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); - mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + mPrograms = (List<Program>) BigArguments.getArgument( + SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + BigArguments.reset(); } + if (args == null || mPrograms == null) { + getActivity().finish(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); - singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); mChannelDataManager = singletons.getChannelDataManager(); mChannelDataManager.addListener(mChannelListener); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener); getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); } @@ -144,8 +162,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { private void onProgramsUpdated() { ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); + updateEmptyMessage(); + } + + private void updateEmptyMessage() { if (mPrograms == null || mPrograms.isEmpty()) { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); + if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { + showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state); + } else { + showEmptyMessage(R.string.dvr_series_schedules_empty_state); + } } else { hideEmptyMessage(); } @@ -158,15 +184,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = null; } getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mHandler.removeCallbacksAndMessages(null); mChannelDataManager.removeListener(mChannelListener); - TvApplication.getSingletons(getContext()).getDvrDataManager() - .removeSeriesRecordingListener(mSeriesRecordingListener); + mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); super.onDestroy(); } @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { - return new SeriesRecordingHeaderRowPresenter(getContext()); + return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext()); } @Override @@ -195,7 +221,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { @Override protected void onPostExecute(List<Program> programs) { - mPrograms = programs; + mPrograms = programs == null ? Collections.EMPTY_LIST : programs; onProgramsUpdated(); } }; @@ -205,4 +231,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { .setIgnoreChannelOption(true) .execute(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java index 23aebf59..9b0ad105 100644 --- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -19,13 +19,13 @@ package com.android.tv.dvr.ui.list; import android.content.Context; import com.android.tv.data.Program; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.ScheduledRecording.Builder; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.Builder; /** * A class for the episodic program. */ -public class EpisodicProgramRow extends ScheduleRow { +class EpisodicProgramRow extends ScheduleRow { private final String mInputId; private final Program mProgram; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 3fc92e8a..66e96f94 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -20,12 +20,12 @@ import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * A class for schedule recording row. */ -public class ScheduleRow { +class ScheduleRow { private final SchedulesHeaderRow mHeaderRow; @Nullable private ScheduledRecording mSchedule; private boolean mStopRecordingRequested; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 9cc82653..97d60473 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -30,8 +30,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ -public class ScheduleRowAdapter extends ArrayObjectAdapter { +class ScheduleRowAdapter extends ArrayObjectAdapter { private static final String TAG = "ScheduleRowAdapter"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 1257e725..dc4e3c41 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -42,25 +42,24 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ @TargetApi(Build.VERSION_CODES.N) -public class ScheduleRowPresenter extends RowPresenter { +class ScheduleRowPresenter extends RowPresenter { private static final String TAG = "ScheduleRowPresenter"; @Retention(RetentionPolicy.SOURCE) @@ -345,7 +344,9 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onInfoClicked(row); + if (isInfoClickable(row)) { + onInfoClicked(row); + } } }); @@ -366,8 +367,7 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { - int durationMins = - Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); + int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } @@ -403,6 +403,7 @@ public class ScheduleRowPresenter extends RowPresenter { } else { viewHolder.whiteBackInfo(); } + viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } @@ -454,11 +455,13 @@ public class ScheduleRowPresenter extends RowPresenter { /** * Called when user click Info in {@link ScheduleRow}. */ - protected void onInfoClicked(ScheduleRow scheduleRow) { - ScheduledRecording schedule = scheduleRow.getSchedule(); - if (schedule != null) { - DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); - } + protected void onInfoClicked(ScheduleRow row) { + DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); + } + + private boolean isInfoClickable(ScheduleRow row) { + return row.getSchedule() != null + && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress()); } /** @@ -545,7 +548,7 @@ public class ScheduleRowPresenter extends RowPresenter { // This row has been deleted. return; } - if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) { row.setStopRecordingRequested(true); mDvrManager.stopRecording(row.getSchedule()); CharSequence deletedInfo = onGetProgramInfoText(row); @@ -670,10 +673,9 @@ public class ScheduleRowPresenter extends RowPresenter { hideActionView(viewHolder.mFirstActionContainer, View.GONE); } }; - if (mLastFocusedViewId == R.id.action_first_container - || mLastFocusedViewId == R.id.action_second_container) { - mLastFocusedViewId = R.id.info_container; - } + mLastFocusedViewId = R.id.info_container; + SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG, + "No focusable view in this row: " + viewHolder); break; } View view = viewHolder.view.findViewById(mLastFocusedViewId); @@ -683,8 +685,10 @@ public class ScheduleRowPresenter extends RowPresenter { // requestFocus() explicitly. if (view.hasFocus()) { viewHolder.mPendingAnimationRunnable.run(); - } else { + } else if (view.isFocusable()){ view.requestFocus(); + } else { + viewHolder.view.requestFocus(); } } } else { @@ -737,10 +741,10 @@ public class ScheduleRowPresenter extends RowPresenter { @ScheduleRowAction protected int[] getAvailableActions(ScheduleRow row) { if (row.getSchedule() != null) { - if (row.isOnAir()) { - if (row.isRecordingInProgress()) { - return new int[] {ACTION_STOP_RECORDING}; - } else if (row.isRecordingNotStarted()) { + if (row.isRecordingInProgress()) { + return new int[]{ACTION_STOP_RECORDING}; + } else if (row.isOnAir()) { + if (row.isRecordingNotStarted()) { if (canResolveConflict()) { // The "START" action can change the conflict states. return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index 0fb0924d..715ecb8c 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,12 +16,15 @@ package com.android.tv.dvr.ui.list; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.data.Program; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.List; /** * A base class for the rows for schedules' header. */ -public abstract class SchedulesHeaderRow { +abstract class SchedulesHeaderRow { private String mTitle; private String mDescription; private int mItemCount; @@ -98,11 +101,20 @@ public abstract class SchedulesHeaderRow { */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { private SeriesRecording mSeriesRecording; + private List<Program> mPrograms; public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series) { + SeriesRecording series, List<Program> programs) { super(title, description, itemCount); mSeriesRecording = series; + mPrograms = programs; + } + + /** + * Returns the list of programs which belong to the series. + */ + public List<Program> getPrograms() { + return mPrograms; } /** @@ -119,4 +131,4 @@ public abstract class SchedulesHeaderRow { mSeriesRecording = seriesRecording; } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index 69c33a96..fe2033ba 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -30,15 +30,14 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.DvrSchedulesActivity; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ -public abstract class SchedulesHeaderRowPresenter extends RowPresenter { +abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; public SchedulesHeaderRowPresenter(Context context) { @@ -79,7 +78,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter { public DateHeaderRowPresenter(Context context) { @@ -93,7 +92,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { /** * A ViewHolder for - * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { public DateHeaderRowViewHolder(Context context, ViewGroup parent) { @@ -152,9 +151,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } }); headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @@ -169,9 +168,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { .build(); TvApplication.getSingletons(getContext()).getDvrManager() .updateSeriesRecording(seriesRecording); - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } else { DvrUiHelper.showCancelAllSeriesRecordingDialog( (DvrSchedulesActivity) view.getContext(), @@ -182,11 +181,8 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } private void setTextDrawable(TextView textView, Drawable drawableStart) { - if (mLtr) { - textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null); - } else { - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null); - } + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null, + null); } /** diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 3b493774..6b6de8b8 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; @@ -46,7 +46,7 @@ import java.util.Map; * An adapter for series schedule row. */ @TargetApi(Build.VERSION_CODES.N) -public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { +class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private static final String TAG = "SeriesRowAdapter"; private static final boolean DEBUG = false; @@ -96,7 +96,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { Collections.sort(sortedPrograms); List<EpisodicProgramRow> rows = new ArrayList<>(); mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - null, sortedPrograms.size(), mSeriesRecording); + null, sortedPrograms.size(), mSeriesRecording, programs); for (Program program : sortedPrograms) { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(program.getId()); @@ -145,7 +145,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (index != -1) { EpisodicProgramRow row = (EpisodicProgramRow) get(index); if (!row.isStartRecordingRequested()) { - row.setSchedule(schedule); + setScheduleToRow(row, schedule); notifyArrayItemRangeChanged(index, 1); } } @@ -195,12 +195,10 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (!isStartOrStopRequested()) { executePendingUpdate(); } - row.setSchedule(schedule); + setScheduleToRow(row, schedule); } - } else if (willBeKept(schedule)) { - row.setSchedule(schedule); } else { - row.setSchedule(null); + setScheduleToRow(row, schedule); } notifyArrayItemRangeChanged(index, 1); } @@ -213,6 +211,14 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { } } + private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) { + if (schedule != null && willBeKept(schedule)) { + row.setSchedule(schedule); + } else { + row.setSchedule(null); + } + } + private int findRowIndexByProgramId(long programId) { for (int i = 0; i < size(); i++) { Object item = get(i); diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index 5d88579a..c8503e0d 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -22,13 +22,13 @@ import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ -public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { +class SeriesScheduleRowPresenter extends ScheduleRowPresenter { private static final String TAG = "SeriesRowPresenter"; private boolean mLtr; @@ -74,13 +74,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( R.dimen.dvr_schedules_warning_icon_padding)); - if (mLtr) { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - R.drawable.ic_warning_gray600_36dp, 0, 0, 0); - } else { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - 0, 0, R.drawable.ic_warning_gray600_36dp, 0); - } + viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_warning_gray600_36dp, 0, 0, 0); } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } @@ -88,9 +83,7 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected void onInfoClicked(ScheduleRow row) { - if (row.getSchedule() != null) { - DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); - } + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); } @Override diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java index 5deda44a..2437d1f5 100644 --- a/src/com/android/tv/dvr/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.app.Activity; import android.content.Intent; @@ -24,7 +24,7 @@ import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; +import com.android.tv.dvr.data.RecordedProgram; /** * Activity to play a {@link RecordedProgram}. diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java index 8c4c856c..4bd121b1 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.playback; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.view.View; @@ -26,22 +25,25 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.DvrPlaybackActivity; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; +import com.android.tv.dvr.ui.browse.RecordingCardView; import com.android.tv.util.Utils; /** * This class is used to generate Views and bind Objects for related recordings in DVR playback. */ -public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { +class DvrPlaybackCardPresenter extends RecordedProgramPresenter { private static final String TAG = "DvrPlaybackCardPresenter"; private static final boolean DEBUG = false; private final int mRelatedRecordingCardWidth; private final int mRelatedRecordingCardHeight; + private final DvrPlaybackOverlayFragment mFragment; - DvrPlaybackCardPresenter(Context context) { + DvrPlaybackCardPresenter(Context context, DvrPlaybackOverlayFragment fragment) { super(context); + mFragment = fragment; mRelatedRecordingCardWidth = context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); mRelatedRecordingCardHeight = @@ -50,9 +52,8 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { - Resources res = parent.getResources(); RecordingCardView view = new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight); + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true); return new ViewHolder(view); } @@ -61,6 +62,10 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { return new OnClickListener() { @Override public void onClick(View v) { + // Disable fading of overlay fragment to prevent the layout blinking while updating + // new playback states and info. The fading enabled status will be reset during + // playback state changing, in DvrPlaybackControlHelper.onStateChanged(). + mFragment.setFadingEnabled(false); long programId = ((RecordedProgram) v.getTag()).getId(); if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); @@ -69,14 +74,4 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { } }; } - - @Override - protected String getDescription(RecordedProgram program) { - String description = program.getDescription(); - if (TextUtils.isEmpty(description)) { - description = - getContext().getResources().getString(R.string.dvr_msg_no_program_description); - } - return description; - } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java index 0bc4ecb1..4658a328 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -14,19 +14,26 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.playback; import android.app.Activity; +import android.content.Context; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaController.TransportControls; import android.media.session.PlaybackState; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; import android.support.v17.leanback.app.PlaybackControlGlue; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ControlButtonPresenterSelector; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; +import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.RowPresenter; import android.text.TextUtils; @@ -37,19 +44,18 @@ import android.view.View; import com.android.tv.R; import com.android.tv.util.TimeShiftUtils; +import java.util.ArrayList; + /** * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and * send command to the media controller. It also helps to update playback states displayed in the * fragment according to information the media session provides. */ -public class DvrPlaybackControlHelper extends PlaybackControlGlue { +class DvrPlaybackControlHelper extends PlaybackControlGlue { private static final String TAG = "DvrPlaybackControlHelper"; private static final boolean DEBUG = false; - /** - * Indicates the ID of the media under playback is unknown. - */ - public static int UNKNOWN_MEDIA_ID = -1; + private static final int AUDIO_ACTION_ID = 1001; private int mPlaybackState = PlaybackState.STATE_NONE; private int mPlaybackSpeedLevel; @@ -60,6 +66,9 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); private final TransportControls mTransportControls; private final int mExtraPaddingTopForNoDescription; + private final ArrayObjectAdapter mSecondaryActionsAdapter; + private final MultiAction mClosedCaptioningAction; + private final MultiAction mMultiAudioAction; public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); @@ -68,11 +77,15 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls = mMediaController.getTransportControls(); mExtraPaddingTopForNoDescription = activity.getResources() .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); + mSecondaryActionsAdapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector()); + mClosedCaptioningAction = new ClosedCaptioningAction(activity); + mMultiAudioAction = new MultiAudioAction(activity); } @Override public PlaybackControlsRowPresenter createControlsRowAndPresenter() { PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); + controlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); setControlsRow(controlsRow); AbstractDetailsDescriptionPresenter detailsPresenter = new AbstractDetailsDescriptionPresenter() { @@ -116,7 +129,21 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public void onActionClicked(Action action) { if (mReadyToControl) { - DvrPlaybackControlHelper.super.onActionClicked(action); + int trackType; + if (action.getId() == mClosedCaptioningAction.getId()) { + trackType = TvTrackInfo.TYPE_SUBTITLE; + } else if (action.getId() == AUDIO_ACTION_ID) { + trackType = TvTrackInfo.TYPE_AUDIO; + } else { + DvrPlaybackControlHelper.super.onActionClicked(action); + return; + } + ArrayList<TvTrackInfo> trackInfos = + ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType); + if (!trackInfos.isEmpty()) { + showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment) + getFragment()).getSelectedTrackId(trackType)); + } } } }); @@ -158,10 +185,10 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { /** * Returns the ID of the media under playback. */ - public long getMediaId() { + public String getMediaId() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? UNKNOWN_MEDIA_ID - : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID); + return mediaMetadata == null ? null + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); } @Override @@ -217,6 +244,37 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { mMediaController.unregisterCallback(mMediaControllerCallback); } + /** + * Update the secondary controls row. + * @param hasClosedCaption {@code true} to show the closed caption selection button, + * {@code false} to hide it. + * @param hasMultiAudio {@code true} to show the audio track selection button, + * {@code false} to hide it. + */ + public void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { + if (hasClosedCaption) { + if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { + mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); + } + } else { + mSecondaryActionsAdapter.remove(mClosedCaptioningAction); + } + if (hasMultiAudio) { + if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { + mSecondaryActionsAdapter.add(mMultiAudioAction); + } + } else { + mSecondaryActionsAdapter.remove(mMultiAudioAction); + } + } + + /** + * Returns if the secondary controls row has any buttons and thus should be shown. + */ + public boolean hasSecondaryRow() { + return mSecondaryActionsAdapter.size() != 0; + } + @Override protected void startPlayback(int speedId) { if (getCurrentSpeedId() == speedId) { @@ -251,6 +309,14 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { // Do nothing. } + /** + * Notifies closed caption being enabled/disabled to update related UI. + */ + void onSubtitleTrackStateChanged(boolean enabled) { + mClosedCaptioningAction.setIndex(enabled ? + ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); + } + private void onStateChanged(int state, long positionMs, int speedLevel) { if (DEBUG) Log.d(TAG, "onStateChanged"); getControlsRow().setCurrentTime((int) positionMs); @@ -297,6 +363,19 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { onStateChanged(); } + private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) { + Bundle args = new Bundle(); + args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); + args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); + DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); + sideFragment.setArguments(args); + getFragment().getFragmentManager().beginTransaction() + .hide(getFragment()) + .replace(R.id.dvr_playback_side_fragment, sideFragment) + .addToBackStack(null) + .commit(); + } + private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(PlaybackState state) { @@ -310,4 +389,11 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); } } + + private static class MultiAudioAction extends MultiAction { + MultiAudioAction(Context context) { + super(AUDIO_ACTION_ID); + setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); + } + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java index 9759a856..843d2dbe 100644 --- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.app.Activity; import android.content.Intent; @@ -31,14 +31,16 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; -public class DvrPlaybackMediaSessionHelper { +class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; private static final boolean DEBUG = false; @@ -102,6 +104,7 @@ public class DvrPlaybackMediaSessionHelper { } if (mMediaSession != null) { mMediaSession.release(); + mMediaSession = null; } } @@ -179,83 +182,88 @@ public class DvrPlaybackMediaSessionHelper { cardTitleText = (channel != null) ? channel.getDisplayName() : mActivity.getString(R.string.no_program_information); } - updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(), - mProgramDurationMs, null, 0); + final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, + program.getDescription(), mProgramDurationMs); String posterArtUri = program.getPosterArtUri(); if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); } - updatePosterArt(program, cardTitleText, program.getDescription(), - mProgramDurationMs, null, posterArtUri); + updatePosterArt(program, currentMetadata, null, posterArtUri); mMediaSession.setActive(true); } - private void updatePosterArt(RecordedProgram program, String cardTitleText, - String cardSubtitleText, long duration, + private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, @Nullable Bitmap posterArt, @Nullable String posterArtUri) { if (posterArt != null) { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, posterArt, 0); + updateMetadataImageInfo(program, currentMetadata, posterArt, 0); } else if (posterArtUri != null) { ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, - mNowPlayingCardHeight, new ProgramPosterArtCallback( - mActivity, program, cardTitleText, cardSubtitleText, duration)); + mNowPlayingCardHeight, + new ProgramPosterArtCallback(mActivity, program, currentMetadata)); } else { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, null, R.drawable.default_now_card); + updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); } } private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> { - private RecordedProgram mRecordedProgram; - private String mCardTitleText; - private String mCardSubtitleText; - private long mDuration; + private final RecordedProgram mRecordedProgram; + private final MediaMetadata mCurrentMetadata; public ProgramPosterArtCallback(Activity activity, RecordedProgram program, - String cardTitleText, String cardSubtitleText, long duration) { + MediaMetadata metadata) { super(activity); mRecordedProgram = program; - mCardTitleText = cardTitleText; - mCardSubtitleText = cardSubtitleText; - mDuration = duration; + mCurrentMetadata = metadata; } @Override public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { if (isCurrentProgram(mRecordedProgram)) { - updatePosterArt(mRecordedProgram, mCardTitleText, - mCardSubtitleText, mDuration, posterArt, null); + updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); } } } - private void updateMediaMetadata(final long programId, final String title, - final String subtitle, final long duration, - final Bitmap posterArt, final int imageResId) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... arg0) { - MediaMetadata.Builder builder = new MediaMetadata.Builder(); - builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId) - .putString(MediaMetadata.METADATA_KEY_TITLE, title) - .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); - if (subtitle != null) { - builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); - } - Bitmap programPosterArt = posterArt; - if (programPosterArt == null && imageResId != 0) { - programPosterArt = - BitmapFactory.decodeResource(mActivity.getResources(), imageResId); - } - if (programPosterArt != null) { - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); - } + private MediaMetadata updateMetadataTextInfo(final long programId, final String title, + final String subtitle, final long duration) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); + if (subtitle != null) { + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + MediaMetadata metadata = builder.build(); + mMediaSession.setMetadata(metadata); + return metadata; + } + + private void updateMetadataImageInfo(final RecordedProgram program, + final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { + if (mMediaSession != null && (posterArt != null || imageResId != 0)) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); + if (posterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); mMediaSession.setMetadata(builder.build()); - return null; + } else { + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(Void... arg0) { + return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); + } + + @Override + protected void onPostExecute(Bitmap programPosterArt) { + if (mMediaSession != null && programPosterArt != null + && isCurrentProgram(program)) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); + mMediaSession.setMetadata(builder.build()); + } + } + }.execute(); } - }.execute(); + } } // An event was triggered by MediaController.TransportControls and must be handled here. diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java index 51ec93b8..ff907182 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -14,13 +14,14 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.playback; import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.hardware.display.DisplayManager; import android.media.tv.TvContentRating; +import android.media.tv.TvTrackInfo; import android.os.Bundle; import android.media.session.PlaybackState; import android.media.tv.TvInputManager; @@ -30,7 +31,6 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.SinglePresenterSelector; @@ -38,20 +38,25 @@ import android.view.Display; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; -import android.text.TextUtils; import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrPlayer; -import com.android.tv.dvr.DvrPlaybackMediaSessionHelper; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.dvr.ui.browse.DvrListRowPresenter; import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.util.TvSettings; +import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; +import java.util.List; +import java.util.ArrayList; + public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { // TODO: Handles audio focus. Deals with block and ratings. private static final String TAG = "DvrPlaybackOverlayFragment"; @@ -62,6 +67,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { // mProgram is only used to store program from intent. Don't use it elsewhere. private RecordedProgram mProgram; + private DvrPlayer mDvrPlayer; private DvrPlaybackMediaSessionHelper mMediaSessionHelper; private DvrPlaybackControlHelper mPlaybackControlHelper; private ArrayObjectAdapter mRowsAdapter; @@ -72,19 +78,30 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private TvView mTvView; private View mBlockScreenView; private ListRow mRelatedRecordingsRow; - private int mExtraPaddingNoRelatedRow; + private int mPaddingWithoutRelatedRow; + private int mPaddingWithoutSecondaryRow; private int mWindowWidth; private int mWindowHeight; private float mAppliedAspectRatio; private float mWindowAspectRatio; private boolean mPinChecked; + private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = + new DvrPlayer.OnTrackSelectedListener() { + @Override + public void onTrackSelected(String selectedTrackId) { + mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); + mRowsAdapter.notifyArrayItemRangeChanged(0, 1); + } + }; @Override public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); - mExtraPaddingNoRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top); + mPaddingWithoutRelatedRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); + mPaddingWithoutSecondaryRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); mContentRatingsManager = TvApplication.getSingletons(getContext()) .getTvInputManagerHelper().getContentRatingsManager(); @@ -110,13 +127,31 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { super.onActivityCreated(savedInstanceState); mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); + mDvrPlayer = new DvrPlayer(mTvView); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); setUpRows(); - preparePlayback(getActivity().getIntent()); - DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer(); - dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() { + mDvrPlayer.setOnTracksAvailabilityChangedListener( + new DvrPlayer.OnTracksAvailabilityChangedListener() { + @Override + public void onTracksAvailabilityChanged(boolean hasClosedCaption, + boolean hasMultiAudio) { + mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); + if (hasClosedCaption) { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, + mOnSubtitleTrackSelectedListener); + selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); + } else { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); + } + if (hasMultiAudio) { + selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); + } + onMediaControllerUpdated(); + } + }); + mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { @Override public void onAspectRatioChanged(float videoAspectRatio) { updateAspectRatio(videoAspectRatio); @@ -124,7 +159,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { }); mPinChecked = getActivity().getIntent() .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); - dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() { + mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() { @Override public void onContentBlocked(TvContentRating rating) { if (mPinChecked) { @@ -149,6 +184,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); } }); + preparePlayback(getActivity().getIntent()); } @Override @@ -200,6 +236,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { updateAspectRatio(mAppliedAspectRatio); } + /** + * Returns next recorded episode in the same series as now playing program. + */ public RecordedProgram getNextEpisode(RecordedProgram program) { int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); if (position == mRelatedRecordingsRowAdapter.size()) { @@ -209,16 +248,92 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } } + /** + * Returns the tracks of the give type of the current playback. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. + */ + public ArrayList<TvTrackInfo> getTracks(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mDvrPlayer.getAudioTracks(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mDvrPlayer.getSubtitleTracks(); + } + return null; + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + return mDvrPlayer.getSelectedTrackId(trackType); + } + + /** + * Returns the language setting of the given track type. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @return {@code null} if no language has been set for the given track type. + */ + TvTrackInfo getTrackSetting(int trackType) { + return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); + } + + /** + * Selects the given audio or subtitle track for DVR playback. + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @param selectedTrack {@code null} to disable the audio or subtitle track according to + * trackType. + */ + void selectTrack(int trackType, TvTrackInfo selectedTrack) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.selectTrack(trackType, selectedTrack); + } + } + + /** + * Notifies the content of controls row or related recordings row is changed and the UI should + * be updated according to the change. + */ void onMediaControllerUpdated() { - mRowsAdapter.notifyArrayItemRangeChanged(0, 1); + updateVerticalPosition(); + mRowsAdapter.notifyArrayItemRangeChanged(0, 2); + } + + private void selectBestMatchedTrack(int trackType) { + TvTrackInfo selectedTrack = getTrackSetting(trackType); + if (selectedTrack != null) { + TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), + selectedTrack.getId(), selectedTrack.getLanguage(), + trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); + if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils + .isEqualLanguage(bestMatchedTrack.getLanguage(), + selectedTrack.getLanguage()))) { + selectTrack(trackType, bestMatchedTrack); + return; + } + } + if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + // Disables closed captioning if there's no matched language. + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); + } } private void updateAspectRatio(float videoAspectRatio) { + if (videoAspectRatio <= 0) { + // We don't have video's width or height information, use window's aspect ratio. + videoAspectRatio = mWindowAspectRatio; + } if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { // No need to change return; } - if (videoAspectRatio < mWindowAspectRatio) { + if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); + } else if (videoAspectRatio < mWindowAspectRatio) { int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); } else { @@ -230,6 +345,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private void preparePlayback(Intent intent) { mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); + mPlaybackControlHelper.updateSecondaryRow(false, false); getActivity().getMediaController().getTransportControls().prepare(); updateRelatedRecordingsRow(); } @@ -239,24 +355,35 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { mRelatedRecordingsRowAdapter.clear(); long programId = mProgram.getId(); String seriesId = mProgram.getSeriesId(); - if (!TextUtils.isEmpty(seriesId)) { + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + if (seriesRecording != null) { if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); - for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) { - if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) { + List<RecordedProgram> relatedPrograms = + mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); + for (RecordedProgram program : relatedPrograms) { + if (programId != program.getId()) { mRelatedRecordingsRowAdapter.add(program); } } } - View view = getView(); if (mRelatedRecordingsRowAdapter.size() == 0) { mRowsAdapter.remove(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow, - view.getPaddingRight(), view.getPaddingBottom()); } else if (wasEmpty){ mRowsAdapter.add(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), 0, - view.getPaddingRight(), view.getPaddingBottom()); } + onMediaControllerUpdated(); + } + + private void updateVerticalPosition() { + int verticalPadding = 0; + verticalPadding += + mRelatedRecordingsRowAdapter.size() == 0 ? mPaddingWithoutRelatedRow : 0; + verticalPadding += + mPlaybackControlHelper.hasSecondaryRow() ? 0 : mPaddingWithoutSecondaryRow; + if (DEBUG) Log.d(TAG, "New controls padding: " + verticalPadding); + View view = getView(); + view.setPadding(view.getPaddingLeft(), verticalPadding, + view.getPaddingRight(), view.getPaddingBottom()); } private void setUpRows() { @@ -265,7 +392,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { ClassPresenterSelector selector = new ClassPresenterSelector(); selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); - selector.addClassPresenter(ListRow.class, new ListRowPresenter()); + selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); mRowsAdapter = new ArrayObjectAdapter(selector); mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); @@ -274,7 +401,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } private ListRow getRelatedRecordingsRow() { - mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this); mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); HeaderItem header = new HeaderItem(0, getActivity().getString(R.string.dvr_playback_related_recordings)); @@ -297,7 +424,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } @Override - long getId(BaseProgram item) { + public long getId(BaseProgram item) { return item.getId(); } } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java new file mode 100644 index 00000000..e49870f1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; +import android.transition.Transition; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; + +import java.util.List; +import java.util.Locale; + +/** + * Fragment for DVR playback closed-caption/multi-audio settings. + */ +public class DvrPlaybackSideFragment extends GuidedStepFragment { + /** + * The tag for passing track infos to side fragments. + */ + public static final String TRACK_INFOS = "dvr_key_track_infos"; + /** + * The tag for passing selected track's ID to side fragments. + */ + public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; + + private static final int ACTION_ID_NO_SUBTITLE = -1; + private static final int CHECK_SET_ID = 1; + + private List<TvTrackInfo> mTrackInfos; + private String mSelectedTrackId; + private TvTrackInfo mSelectedTrack; + private int mTrackType; + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); + mTrackType = mTrackInfos.get(0).getType(); + mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); + mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment)); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); + backgroundView.setBackgroundColor(getResources() + .getColor(R.color.lb_playback_controls_background_light)); + return backgroundView; + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_NO_SUBTITLE) + .title(getString(R.string.closed_caption_option_item_off)) + .checkSetId(CHECK_SET_ID) + .checked(mSelectedTrackId == null) + .build()); + } + for (int i = 0; i < mTrackInfos.size(); i++) { + TvTrackInfo info = mTrackInfos.get(i); + boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); + GuidedAction action = new GuidedAction.Builder(getActivity()) + .id(i) + .title(getTrackLabel(info, i)) + .checkSetId(CHECK_SET_ID) + .checked(checked) + .build(); + actions.add(action); + if (checked) { + mSelectedTrack = info; + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + int actionId = (int) action.getId(); + mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId)); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + int actionId = (int) action.getId(); + mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId); + TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack); + getFragmentManager().popBackStack(); + } + + @Override + public void onStart() { + super.onStart(); + // Workaround: when overlay fragment is faded out, any focus will lost due to overlay + // fragment's implementation. So we disable overlay fragment's fading here to prevent + // losing focus while users are interacting with the side fragment. + mOverlayFragment.setFadingEnabled(false); + } + + @Override + public void onStop() { + super.onStop(); + // We disable fading of overlay fragment to prevent side fragment from losing focus, + // therefore we should resume it here. + mOverlayFragment.setFadingEnabled(true); + mOverlayFragment.selectTrack(mTrackType, mSelectedTrack); + } + + private String getTrackLabel(TvTrackInfo track, int trackIndex) { + if (track.getLanguage() != null) { + return new Locale(track.getLanguage()).getDisplayName(); + } + return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? + getString(R.string.closed_caption_unknown_language, trackIndex + 1) + : getString(R.string.multi_audio_unknown_language); + } + + @Override + protected void onProvideFragmentTransitions() { + super.onProvideFragmentTransitions(); + // Excludes the background scrim from transition to prevent the blinking caused by + // hiding the overlay fragment and sliding in the side fragment at the same time. + Transition t = getEnterTransition(); + if (t != null) { + t.excludeTarget(R.id.guidedstep_background, true); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java index 5656655c..780bfb2f 100644 --- a/src/com/android/tv/dvr/DvrPlayer.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.media.PlaybackParams; import android.media.tv.TvContentRating; @@ -22,12 +22,16 @@ import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.session.PlaybackState; +import android.text.TextUtils; import android.util.Log; +import com.android.tv.dvr.data.RecordedProgram; + +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -public class DvrPlayer { +class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; @@ -47,12 +51,19 @@ public class DvrPlayer { private long mInitialSeekPositionMs; private final TvView mTvView; private DvrPlayerCallback mCallback; - private AspectRatioChangedListener mAspectRatioChangedListener; - private ContentBlockedListener mContentBlockedListener; + private OnAspectRatioChangedListener mOnAspectRatioChangedListener; + private OnContentBlockedListener mOnContentBlockedListener; + private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; + private OnTrackSelectedListener mOnAudioTrackSelectedListener; + private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; + private String mSelectedAudioTrackId; + private String mSelectedSubtitleTrackId; private float mAspectRatio = Float.NaN; private int mPlaybackState = PlaybackState.STATE_NONE; private long mTimeShiftCurrentPositionMs; private boolean mPauseOnPrepared; + private boolean mHasClosedCaption; + private boolean mHasMultiAudio; private final PlaybackParams mPlaybackParams = new PlaybackParams(); private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -75,22 +86,40 @@ public class DvrPlayer { public void onPlaybackEnded() { } } - public interface AspectRatioChangedListener { + public interface OnAspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. + * + * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. + * Listeners should handle it carefully. */ void onAspectRatioChanged(float videoAspectRatio); } - public interface ContentBlockedListener { + public interface OnContentBlockedListener { /** * Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } + public interface OnTracksAvailabilityChangedListener { + /** + * Called when the Video's subtitle or audio tracks are changed. + */ + void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); + } + + public interface OnTrackSelectedListener { + /** + * Called when certain subtitle or audio track is selected. + */ + void onTrackSelected(String selectedTrackId); + } + public DvrPlayer(TvView tvView) { mTvView = tvView; + mTvView.setCaptionEnabled(true); mPlaybackParams.setSpeed(1.0f); setTvViewCallbacks(); setCallback(null); @@ -236,6 +265,8 @@ public class DvrPlayer { mTimeShiftCurrentPositionMs = 0; mPlaybackParams.setSpeed(1.0f); mProgram = null; + mSelectedAudioTrackId = null; + mSelectedSubtitleTrackId = null; } /** @@ -250,17 +281,51 @@ public class DvrPlayer { } /** - * Sets listener to aspect ratio changing. + * Sets the listener to aspect ratio changing. + */ + public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { + mOnAspectRatioChangedListener = listener; + } + + /** + * Sets the listener to content blocking. + */ + public void setOnContentBlockedListener(OnContentBlockedListener listener) { + mOnContentBlockedListener = listener; + } + + /** + * Sets the listener to tracks changing. + */ + public void setOnTracksAvailabilityChangedListener( + OnTracksAvailabilityChangedListener listener) { + mOnTracksAvailabilityChangedListener = listener; + } + + /** + * Sets the listener to tracks of the given type being selected. + * + * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} + * or {@link TvTrackInfo#TYPE_SUBTITLE}. */ - public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { - mAspectRatioChangedListener = listener; + public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mOnAudioTrackSelectedListener = listener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mOnSubtitleTrackSelectedListener = listener; + } } /** - * Sets listener to content blocking. + * Gets the listener to tracks of the given type being selected. */ - public void setContentBlockedListener(ContentBlockedListener listener) { - mContentBlockedListener = listener; + public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mOnAudioTrackSelectedListener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mOnSubtitleTrackSelectedListener; + } + return null; } /** @@ -306,6 +371,32 @@ public class DvrPlayer { } /** + * Returns the subtitle tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getSubtitleTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); + } + + /** + * Returns the audio tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getAudioTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mSelectedAudioTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mSelectedSubtitleTrackId; + } + return null; + } + + /** * Returns if playback of the recorded program is started. */ public boolean isPlaybackPrepared() { @@ -313,6 +404,41 @@ public class DvrPlayer { && mPlaybackState != PlaybackState.STATE_CONNECTING; } + /** + * Selects the given track. + * + * @return ID of the selected track. + */ + String selectTrack(int trackType, TvTrackInfo selectedTrack) { + String oldSelectedTrackId = getSelectedTrackId(trackType); + String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); + if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { + if (selectedTrack == null) { + mTvView.selectTrack(trackType, null); + return null; + } else { + List<TvTrackInfo> tracks = mTvView.getTracks(trackType); + if (tracks != null && tracks.contains(selectedTrack)) { + mTvView.selectTrack(trackType, newSelectedTrackId); + return newSelectedTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { + // Track not found, disabled closed caption. + mTvView.selectTrack(trackType, null); + return null; + } + } + } + return oldSelectedTrackId; + } + + private void setSelectedTrackId(int trackType, String trackId) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mSelectedAudioTrackId = trackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mSelectedSubtitleTrackId = trackId; + } + } + private void setPlaybackSpeed(int speed) { mPlaybackParams.setSpeed(speed); mTvView.timeShiftSetPlaybackParams(mPlaybackParams); @@ -369,28 +495,60 @@ public class DvrPlayer { if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE && mPlaybackState == PlaybackState.STATE_CONNECTING) { mTimeShiftPlayAvailable = true; + if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + // onTimeShiftStatusChanged is sometimes called after + // onTimeShiftStartPositionChanged is called. In this case, + // resumeToWatchedPositionIfNeeded needs to be called here. + resumeToWatchedPositionIfNeeded(); + } } } @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (trackId == null || type != TvTrackInfo.TYPE_VIDEO - || mAspectRatioChangedListener == null) { - return; + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + boolean hasClosedCaption = + !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); + boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; + if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) + && mOnTracksAvailabilityChangedListener != null) { + mOnTracksAvailabilityChangedListener + .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); } - List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); - if (trackInfos != null) { - for (TvTrackInfo trackInfo : trackInfos) { - if (trackInfo.getId().equals(trackId)) { - float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() - * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); - if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); - if (!Float.isNaN(videoAspectRatio) - && mAspectRatio != videoAspectRatio) { - mAspectRatioChangedListener - .onAspectRatioChanged(videoAspectRatio); - mAspectRatio = videoAspectRatio; - return; + mHasClosedCaption = hasClosedCaption; + mHasMultiAudio = hasMultiAudio; + } + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { + setSelectedTrackId(type, trackId); + OnTrackSelectedListener listener = getOnTrackSelectedListener(type); + if (listener != null) { + listener.onTrackSelected(trackId); + } + } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null + && mOnAspectRatioChangedListener != null) { + List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio; + int videoWidth = trackInfo.getVideoWidth(); + int videoHeight = trackInfo.getVideoHeight(); + if (videoWidth > 0 && videoHeight > 0) { + videoAspectRatio = trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); + } else { + // Aspect ratio is unknown. Pass the message to listeners. + videoAspectRatio = 0; + } + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { + mOnAspectRatioChangedListener + .onAspectRatioChanged(videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; + } } } } @@ -399,8 +557,8 @@ public class DvrPlayer { @Override public void onContentBlocked(String inputId, TvContentRating rating) { - if (mContentBlockedListener != null) { - mContentBlockedListener.onContentBlocked(rating); + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); } } }); |