diff options
author | Nick Chalko <nchalko@google.com> | 2016-10-26 14:03:09 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2016-10-31 10:36:49 -0700 |
commit | d41f0075a7d2ea826204e81fcec57d0aa57171a9 (patch) | |
tree | cb30cfbafd80e01d314868cdc36e783d39981119 /src/com/android/tv/dvr | |
parent | 5e0ec06a797e3497da94390c63c7072de442695b (diff) | |
download | TV-d41f0075a7d2ea826204e81fcec57d0aa57171a9.tar.gz |
Sync to ub-tv-killing at 6f6e46557accb62c9548e4177d6005aa944dbf33
Change-Id: I873644d6d9d0110c981ef6075cb4019c16bbb94b
Diffstat (limited to 'src/com/android/tv/dvr')
71 files changed, 5711 insertions, 2189 deletions
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index 6af77940..89661df3 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -30,9 +30,10 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.util.Clock; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -136,35 +137,35 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } /** - * Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsAdded} * for each listener. */ - protected final void notifyRecordedProgramAdded(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsAdded(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + recordedProgram); - l.onRecordedProgramAdded(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsAdded(recordedPrograms); } } /** - * Calls {@link RecordedProgramListener#onRecordedProgramChanged(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsChanged} * for each listener. */ - protected final void notifyRecordedProgramChanged(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsChanged(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + recordedProgram); - l.onRecordedProgramChanged(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsChanged(recordedPrograms); } } /** - * Calls {@link RecordedProgramListener#onRecordedProgramRemoved(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsRemoved} * for each listener. */ - protected final void notifyRecordedProgramRemoved(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + recordedProgram); - l.onRecordedProgramRemoved(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsRemoved(recordedPrograms); } } @@ -174,7 +175,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingAdded(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingAdded(seriesRecordings); } } @@ -185,7 +186,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingRemoved(seriesRecordings); } } @@ -197,7 +198,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingChanged(seriesRecordings); } } @@ -208,7 +209,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingAdded(scheduledRecording); } } @@ -219,7 +220,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingRemoved(scheduledRecording); } } @@ -232,7 +233,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { protected final void notifyScheduledRecordingStatusChanged( ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingStatusChanged(scheduledRecording); } } @@ -259,14 +260,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override - public List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings() { - return filterEndTimeIsPast(getRecordingsWithState( - ScheduledRecording.STATE_RECORDING_IN_PROGRESS, - ScheduledRecording.STATE_RECORDING_NOT_STARTED, - ScheduledRecording.STATE_RECORDING_CANCELED)); - } - - @Override public List<ScheduledRecording> getStartedRecordings() { return filterEndTimeIsPast(getRecordingsWithState( ScheduledRecording.STATE_RECORDING_IN_PROGRESS)); @@ -274,8 +267,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { @Override public List<ScheduledRecording> getNonStartedScheduledRecordings() { - Set<Integer> states = new HashSet<>(); - states.add(ScheduledRecording.STATE_RECORDING_NOT_STARTED); return filterEndTimeIsPast(getRecordingsWithState( ScheduledRecording.STATE_RECORDING_NOT_STARTED)); } @@ -314,6 +305,9 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { @Override public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) { SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId); + if (seriesRecording == null) { + return Collections.emptyList(); + } List<RecordedProgram> result = new ArrayList<>(); for (RecordedProgram r : getRecordedPrograms()) { if (seriesRecording.getSeriesId().equals(r.getSeriesId())) { @@ -322,4 +316,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } return result; } + + @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 126f3e74..06613667 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -69,11 +69,6 @@ public interface DvrDataManager { List<ScheduledRecording> getAvailableScheduledRecordings(); /** - * Return all available and canceled {@link ScheduledRecording}. - */ - List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings(); - - /** * Returns started recordings that expired. */ List<ScheduledRecording> getStartedRecordings(); @@ -260,10 +255,10 @@ public interface DvrDataManager { * Listens for changes to {@link RecordedProgram}s. */ interface RecordedProgramListener { - void onRecordedProgramAdded(RecordedProgram recordedProgram); + void onRecordedProgramsAdded(RecordedProgram... recordedPrograms); - void onRecordedProgramChanged(RecordedProgram recordedProgram); + void onRecordedProgramsChanged(RecordedProgram... recordedPrograms); - void onRecordedProgramRemoved(RecordedProgram recordedProgram); + void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms); } } diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 5ae2c4ea..46682a48 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -16,12 +16,16 @@ package com.android.tv.dvr; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.ContentObserver; +import android.database.sqlite.SQLiteException; import android.media.tv.TvContract.RecordedPrograms; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager.TvInputCallback; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -37,7 +41,7 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; @@ -47,9 +51,13 @@ 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.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; +import com.android.tv.util.Filter; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvProviderUriMatcher; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -69,6 +77,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private static final String TAG = "DvrDataManagerImpl"; private static final boolean DEBUG = false; + private final TvInputManagerHelper mInputManager; + private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>(); @@ -76,6 +86,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { new HashMap<>(); private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>(); + private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput = + new HashMap<>(); + private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>(); + private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>(); + private final Context mContext; private final ContentObserver mContentObserver = new ContentObserver(new Handler( Looper.getMainLooper())) { @@ -96,15 +111,68 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private boolean mDvrLoadFinished; private boolean mRecordedProgramLoadFinished; private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); - private final DvrDbSync mDbSync; + private DvrDbSync mDbSync; + private DvrStorageStatusManager mStorageStatusManager; + + private final TvInputCallback mInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); + if (!isInputAvailable(inputId)) { + if (DEBUG) Log.d(TAG, "Not available for recording"); + return; + } + unhideInput(inputId); + } + + @Override + public void onInputRemoved(String inputId) { + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); + hideInput(inputId); + } + }; + + private final OnStorageMountChangedListener mStorageMountChangedListener = + new OnStorageMountChangedListener() { + @Override + public void onStorageMountChanged(boolean storageMounted) { + for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) { + if (Utils.isBundledInput(input.getId())) { + if (storageMounted) { + unhideInput(input.getId()); + } else { + hideInput(input.getId()); + } + } + } + } + }; + + private static <T> List<T> moveElements(HashMap<Long, T> from, HashMap<Long, T> to, + Filter<T> filter) { + List<T> moved = new ArrayList<>(); + Iterator<Entry<Long, T>> iter = from.entrySet().iterator(); + while (iter.hasNext()) { + Entry<Long, T> entry = iter.next(); + if (filter.filter(entry.getValue())) { + to.put(entry.getKey(), entry.getValue()); + iter.remove(); + moved.add(entry.getValue()); + } + } + return moved; + } public DvrDataManagerImpl(Context context, Clock clock) { super(context, clock); mContext = context; - mDbSync = new DvrDbSync(context, this); + mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); + mStorageStatusManager = TvApplication.getSingletons(context).getDvrStorageStatusManager(); } public void start() { + mInputManager.addCallback(mInputCallback); + mStorageStatusManager.addListener(mStorageMountChangedListener); AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask = new AsyncDvrQuerySeriesRecordingTask(mContext) { @Override @@ -116,9 +184,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { protected void onPostExecute(List<SeriesRecording> seriesRecordings) { mPendingTasks.remove(this); long maxId = 0; + HashSet<String> seriesIds = new HashSet<>(); for (SeriesRecording r : seriesRecordings) { - mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + if (SoftPreconditions.checkState(!seriesIds.contains(r.getSeriesId()), TAG, + "Skip loading series recording with duplicate series ID: " + r)) { + seriesIds.add(r.getSeriesId()); + if (isInputAvailable(r.getInputId())) { + mSeriesRecordings.put(r.getId(), r); + mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + } else { + mSeriesRecordingsForRemovedInput.put(r.getId(), r); + } + } if (maxId < r.getId()) { maxId = r.getId(); } @@ -128,20 +205,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }; dvrQuerySeriesRecordingTask.executeOnDbThread(); mPendingTasks.add(dvrQuerySeriesRecordingTask); - AsyncDvrQueryScheduleTask dvrQueryRecordingTask + AsyncDvrQueryScheduleTask dvrQueryScheduleTask = new AsyncDvrQueryScheduleTask(mContext) { @Override protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { mPendingTasks.remove(this); } + @SuppressLint("SwitchIntDef") @Override protected void onPostExecute(List<ScheduledRecording> result) { mPendingTasks.remove(this); long maxId = 0; + List<SeriesRecording> seriesRecordingsToAdd = new ArrayList<>(); List<ScheduledRecording> toUpdate = new ArrayList<>(); + List<ScheduledRecording> toDelete = new ArrayList<>(); for (ScheduledRecording r : result) { - if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { + if (!isInputAvailable(r.getInputId())) { + mScheduledRecordingsForRemovedInput.put(r.getId(), r); + } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { getDeletedScheduleMap().put(r.getProgramId(), r); } else { mScheduledRecordings.put(r.getId(), r); @@ -149,22 +231,29 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mProgramId2ScheduledRecordings.put(r.getProgramId(), r); } // Adjust the state of the schedules before DB loading is finished. - if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); - } else { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) - .build()); - } - } else if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); - } + switch (r.getState()) { + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState(ScheduledRecording.STATE_RECORDING_FAILED) + .build()); + } else { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState(ScheduledRecording.STATE_RECORDING_FAILED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_CANCELED: + toDelete.add(r); + break; } } if (maxId < r.getId()) { @@ -172,19 +261,23 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!toUpdate.isEmpty()) { - updateScheduledRecording(true, ScheduledRecording.toArray(toUpdate)); + updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); + } + if (!toDelete.isEmpty()) { + removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); mDbSync.start(); if (isInitialized()) { SeriesRecordingScheduler.getInstance(mContext).start(); } } }; - dvrQueryRecordingTask.executeOnDbThread(); - mPendingTasks.add(dvrQueryRecordingTask); + dvrQueryScheduleTask.executeOnDbThread(); + mPendingTasks.add(dvrQueryScheduleTask); RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(mContext.getContentResolver(), null); mRecordedProgramQueryTask.executeOnDbThread(); @@ -193,8 +286,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } public void stop() { + mInputManager.removeCallback(mInputCallback); + mStorageStatusManager.removeListener(mStorageMountChangedListener); SeriesRecordingScheduler.getInstance(mContext).stop(); - mDbSync.stop(); + if (mDbSync != null) { + mDbSync.stop(); + } ContentResolver cr = mContext.getContentResolver(); cr.unregisterContentObserver(mContentObserver); Iterator<AsyncTask> i = mPendingTasks.iterator(); @@ -213,52 +310,76 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { for (RecordedProgram recorded : recordedPrograms) { - mRecordedPrograms.put(recorded.getId(), recorded); + if (isInputAvailable(recorded.getInputId())) { + mRecordedPrograms.put(recorded.getId(), recorded); + } else { + mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); + } } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { - for (RecordedProgram recorded : mRecordedPrograms.values()) { - notifyRecordedProgramRemoved(recorded); - } + List<RecordedProgram> oldRecordedPrograms = + new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); + mRecordedProgramsForRemovedInput.clear(); + notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms)); } else { HashMap<Long, RecordedProgram> oldRecordedPrograms = new HashMap<>(mRecordedPrograms); mRecordedPrograms.clear(); + mRecordedProgramsForRemovedInput.clear(); + List<RecordedProgram> addedRecordedPrograms = new ArrayList<>(); + List<RecordedProgram> changedRecordedPrograms = new ArrayList<>(); for (RecordedProgram recorded : recordedPrograms) { - mRecordedPrograms.put(recorded.getId(), recorded); - RecordedProgram old = oldRecordedPrograms.remove(recorded.getId()); - if (old == null) { - notifyRecordedProgramAdded(recorded); + if (isInputAvailable(recorded.getInputId())) { + mRecordedPrograms.put(recorded.getId(), recorded); + if (oldRecordedPrograms.remove(recorded.getId()) == null) { + addedRecordedPrograms.add(recorded); + } else { + changedRecordedPrograms.add(recorded); + } } else { - notifyRecordedProgramChanged(recorded); + mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); } } - for (RecordedProgram recorded : oldRecordedPrograms.values()) { - notifyRecordedProgramRemoved(recorded); + if (!addedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms)); + } + if (!changedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms)); + } + if (!oldRecordedPrograms.isEmpty()) { + notifyRecordedProgramsRemoved( + RecordedProgram.toArray(oldRecordedPrograms.values())); } } if (isInitialized()) { SeriesRecordingScheduler.getInstance(mContext).start(); } } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { + if (!mRecordedProgramLoadFinished) { + return; + } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); if (recordedPrograms == null || recordedPrograms.isEmpty()) { + mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { - notifyRecordedProgramRemoved(old); - } else { - Log.w(TAG, "Could not find old version of deleted program #" + id); + notifyRecordedProgramsRemoved(old); } } else { - RecordedProgram newRecorded = recordedPrograms.get(0); - RecordedProgram old = mRecordedPrograms.put(id, newRecorded); - if (old == null) { - notifyRecordedProgramAdded(newRecorded); + RecordedProgram recordedProgram = recordedPrograms.get(0); + if (isInputAvailable(recordedProgram.getInputId())) { + RecordedProgram old = mRecordedPrograms.put(id, recordedProgram); + if (old == null) { + notifyRecordedProgramsAdded(recordedProgram); + } else { + notifyRecordedProgramsChanged(recordedProgram); + } } else { - notifyRecordedProgramChanged(newRecorded); + mRecordedProgramsForRemovedInput.put(id, recordedProgram); } } } @@ -432,7 +553,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Override public void addScheduledRecording(ScheduledRecording... schedules) { for (ScheduledRecording r : schedules) { - r.setId(IdGenerator.SCHEDULED_RECORDING.newId()); + if (r.getId() == ScheduledRecording.ID_NOT_SET) { + r.setId(IdGenerator.SCHEDULED_RECORDING.newId()); + } mScheduledRecordings.put(r.getId(), r); if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(r.getProgramId(), r); @@ -450,7 +573,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { for (SeriesRecording r : seriesRecordings) { r.setId(IdGenerator.SERIES_RECORDING.newId()); mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SoftPreconditions.checkArgument(previousSeries == null, TAG, "Attempt to add series" + + " recording with the duplicate series ID: " + r.getSeriesId()); } if (mDvrLoadFinished) { notifySeriesRecordingAdded(seriesRecordings); @@ -463,18 +588,22 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(false, schedules); } - private void removeScheduledRecording(boolean forceDelete, ScheduledRecording... schedules) { + @Override + public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { - mProgramId2ScheduledRecordings.remove(r.getProgramId()); - } - // If it belongs to the series recording and it's not started yet, do not delete. - // Instead mark deleted. - if (!forceDelete && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET - && r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + getDeletedScheduleMap().remove(r.getId()); + mProgramId2ScheduledRecordings.remove(r.getProgramId()); + boolean isScheduleForRemovedInput = + mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; + // If it belongs to the series recording and it's not started yet, just mark delete + // instead of deleting it. + if (!isScheduleForRemovedInput && !forceRemove + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) { SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET); ScheduledRecording deleted = ScheduledRecording.buildFrom(r) .setState(ScheduledRecording.STATE_RECORDING_DELETED).build(); @@ -538,8 +667,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { updateScheduledRecording(true, schedules); } - private void updateScheduledRecording(boolean updateDb, - final ScheduledRecording... schedules) { + private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List<ScheduledRecording> toUpdate = new ArrayList<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, @@ -550,9 +678,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r); // The channel ID should not be changed. SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId()); - if (DEBUG) Log.d(TAG, "Updating " + oldScheduledRecording + " with " + r); long programId = r.getProgramId(); - if (oldScheduledRecording != null && oldScheduledRecording.getProgramId() != programId + if (oldScheduledRecording.getProgramId() != programId && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings .get(oldScheduledRecording.getProgramId()); @@ -565,6 +692,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mProgramId2ScheduledRecordings.put(programId, r); } } + if (toUpdate.isEmpty()) { + return; + } ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate); if (mDvrLoadFinished) { notifyScheduledRecordingStatusChanged(scheduleArray); @@ -572,17 +702,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } - removeDeletedSchedules(scheduleArray); + removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { - SeriesRecording old = mSeriesRecordings.put(r.getId(), r); - if (old != null) { - mSeriesId2SeriesRecordings.remove(old.getSeriesId()); - } - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); + SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" + + " updated: " + r); } if (mDvrLoadFinished) { notifySeriesRecordingChanged(seriesRecordings); @@ -590,6 +719,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings); } + private boolean isInputAvailable(String inputId) { + return mInputManager.hasTvInputInfo(inputId) + && (!Utils.isBundledInput(inputId) || mStorageStatusManager.isStorageMounted()); + } + private void removeDeletedSchedules(ScheduledRecording... addedSchedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); for (ScheduledRecording r : addedSchedules) { @@ -625,6 +759,148 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void unhideInput(String inputId) { + if (DEBUG) Log.d(TAG, "unhideInput " + inputId); + List<ScheduledRecording> movedSchedules = + moveElements(mScheduledRecordingsForRemovedInput, mScheduledRecordings, + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + 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>() { + @Override + public boolean filter(RecordedProgram r) { + return r.getInputId().equals(inputId); + } + }); + if (!movedSchedules.isEmpty()) { + for (ScheduledRecording schedule : movedSchedules) { + mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); + } + } + if (!movedSeriesRecordings.isEmpty()) { + for (SeriesRecording seriesRecording : movedSeriesRecordings) { + mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); + } + } + // Notify after all the data are moved. + if (!movedSchedules.isEmpty()) { + notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); + } + if (!movedSeriesRecordings.isEmpty()) { + notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings)); + } + if (!movedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms)); + } + } + + private void hideInput(String inputId) { + 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); + } + }); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); + List<RecordedProgram> movedRecordedPrograms = + moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, + new Filter<RecordedProgram>() { + @Override + public boolean filter(RecordedProgram r) { + return r.getInputId().equals(inputId); + } + }); + if (!movedSchedules.isEmpty()) { + for (ScheduledRecording schedule : movedSchedules) { + mProgramId2ScheduledRecordings.remove(schedule.getProgramId()); + } + } + if (!movedSeriesRecordings.isEmpty()) { + for (SeriesRecording seriesRecording : movedSeriesRecordings) { + mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId()); + } + } + // Notify after all the data are moved. + if (!movedSchedules.isEmpty()) { + notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules)); + } + if (!movedSeriesRecordings.isEmpty()) { + notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings)); + } + if (!movedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms)); + } + } + + @Override + public void forgetStorage(String inputId) { + List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); + for (Iterator<ScheduledRecording> i = + mScheduledRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + ScheduledRecording r = i.next(); + if (inputId.equals(r.getInputId())) { + schedulesToDelete.add(r); + i.remove(); + } + } + List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>(); + for (Iterator<SeriesRecording> i = + mSeriesRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + SeriesRecording r = i.next(); + if (inputId.equals(r.getInputId())) { + seriesRecordingsToDelete.add(r); + i.remove(); + } + } + for (Iterator<RecordedProgram> i = + mRecordedProgramsForRemovedInput.values().iterator(); i.hasNext(); ) { + if (inputId.equals(i.next().getInputId())) { + i.remove(); + } + } + new AsyncDeleteScheduleTask(mContext).executeOnDbThread( + ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( + SeriesRecording.toArray(seriesRecordingsToDelete)); + new AsyncDbTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + ContentResolver resolver = mContext.getContentResolver(); + String args[] = { inputId }; + try { + resolver.delete(RecordedPrograms.CONTENT_URI, + RecordedPrograms.COLUMN_INPUT_ID + " = ?", args); + } catch (SQLiteException e) { + Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e); + } + return null; + } + }.executeOnDbThread(); + } + private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java index baa7f3d9..df181455 100644 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ b/src/com/android/tv/dvr/DvrDbSync.java @@ -28,73 +28,132 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; +import android.util.Log; +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.util.AsyncDbTask.AsyncQueryProgramTask; import com.android.tv.util.TvProviderUriMatcher; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Objects; import java.util.Queue; +import java.util.Set; /** * A class to synchronizes DVR DB with TvProvider. + * + * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the + * other tasks are blocked until the current one finishes. As this class performs the low priority + * jobs which take long time, it should not block others if possible. For this reason, only one + * program is queried at a time and others are queued and will be executed on the other + * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask. */ @MainThread @TargetApi(Build.VERSION_CODES.N) class DvrDbSync { + private static final String TAG = "DvrDbSync"; + private static final boolean DEBUG = false; + private final Context mContext; private final DvrDataManagerImpl mDataManager; - private UpdateProgramTask mUpdateProgramTask; + private final ChannelDataManager mChannelDataManager; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); - private final ContentObserver mProgramsContentObserver = new ContentObserver(new Handler( + private QueryProgramTask mQueryProgramTask; + private final SeriesRecordingScheduler mSeriesRecordingScheduler; + private final ContentObserver mContentObserver = new ContentObserver(new Handler( Looper.getMainLooper())) { @SuppressLint("SwitchIntDef") @Override public void onChange(boolean selfChange, Uri uri) { switch (TvProviderUriMatcher.match(uri)) { case TvProviderUriMatcher.MATCH_PROGRAM: + if (DEBUG) Log.d(TAG, "onProgramsUpdated"); onProgramsUpdated(); break; case TvProviderUriMatcher.MATCH_PROGRAM_ID: - addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId( - ContentUris.parseId(uri))); + if (DEBUG) { + Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); + } + onProgramUpdated(ContentUris.parseId(uri)); break; } } }; + + private final ChannelDataManager.Listener mChannelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + start(); + } + + @Override + public void onChannelListUpdated() { + onChannelsUpdated(); + } + + @Override + public void onChannelBrowsableChanged() { } + }; + private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() { @Override public void onScheduledRecordingAdded(ScheduledRecording... schedules) { for (ScheduledRecording schedule : schedules) { addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); } @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { } + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + } + } @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { for (ScheduledRecording schedule : schedules) { mProgramIdQueue.remove(schedule.getProgramId()); + addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); } }; - public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); + } + + @VisibleForTesting + DvrDbSync(Context context, DvrDataManagerImpl dataManager, + ChannelDataManager channelDataManager) { mContext = context; mDataManager = dataManager; + mChannelDataManager = channelDataManager; + mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); } /** * Starts the DB sync. */ public void start() { + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener(mChannelDataManagerListener); + return; + } mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, - mProgramsContentObserver); + mContentObserver); mDataManager.addScheduledRecordingListener(mScheduleListener); + onChannelsUpdated(); onProgramsUpdated(); } @@ -103,17 +162,51 @@ class DvrDbSync { */ public void stop() { mProgramIdQueue.clear(); - if (mUpdateProgramTask != null) { - mUpdateProgramTask.cancel(true); + if (mQueryProgramTask != null) { + mQueryProgramTask.cancel(true); } + mChannelDataManager.removeListener(mChannelDataManagerListener); mDataManager.removeScheduledRecordingListener(mScheduleListener); - mContext.getContentResolver().unregisterContentObserver(mProgramsContentObserver); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + private void onChannelsUpdated() { + List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>(); + for (SeriesRecording r : mDataManager.getSeriesRecordings()) { + if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r) + .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) + .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + } + } + if (!seriesRecordingsToUpdate.isEmpty()) { + mDataManager.updateSeriesRecording( + SeriesRecording.toArray(seriesRecordingsToUpdate)); + } + List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); + for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { + if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + schedulesToRemove.add(r); + mProgramIdQueue.remove(r.getProgramId()); + } + } + if (!schedulesToRemove.isEmpty()) { + mDataManager.removeScheduledRecording( + ScheduledRecording.toArray(schedulesToRemove)); + } } private void onProgramsUpdated() { - for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) { + for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) { addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); + } + + private void onProgramUpdated(long programId) { + addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId)); + startNextUpdateIfNeeded(); } private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) { @@ -125,34 +218,48 @@ class DvrDbSync { && !mProgramIdQueue.contains(programId) && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); mProgramIdQueue.offer(programId); - startNextUpdateIfNeeded(); + // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the + // schedule updates finish. + // Note that the SeriesRecordingScheduler should be paused even though the program to + // check is not episodic because it can be changed to the episodic program after the + // update, which affect the SeriesRecordingScheduler. + mSeriesRecordingScheduler.pauseUpdate(); } } private void startNextUpdateIfNeeded() { - if (mProgramIdQueue.isEmpty()) { + if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) { return; } - if (mUpdateProgramTask == null || mUpdateProgramTask.isCancelled()) { - mUpdateProgramTask = new UpdateProgramTask(mProgramIdQueue.poll()); - mUpdateProgramTask.executeOnDbThread(); + if (!mProgramIdQueue.isEmpty()) { + if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek()); + mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll()); + mQueryProgramTask.executeOnDbThread(); + } else { + mSeriesRecordingScheduler.resumeUpdate(); } } @VisibleForTesting void handleUpdateProgram(Program program, long programId) { + Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>(); ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); if (schedule != null && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { if (program == null) { mDataManager.removeScheduledRecording(schedule); + if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (seriesRecording != null) { + seriesRecordingsToUpdate.add(seriesRecording); + } + } } else { long currentTimeMs = System.currentTimeMillis(); - // Change start time only when the recording start time has not passed. - boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs - && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) .setEndTimeMs(program.getEndTimeUtcMillis()) .setSeasonNumber(program.getSeasonNumber()) @@ -162,10 +269,51 @@ class DvrDbSync { .setProgramLongDescription(program.getLongDescription()) .setProgramPosterArtUri(program.getPosterArtUri()) .setProgramThumbnailUri(program.getThumbnailUri()); + boolean needUpdate = false; + // Check the series recording. + SeriesRecording seriesRecordingForOldSchedule = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (program.getSeriesId() != null) { + // New program belongs to a series. + SeriesRecording seriesRecording = + 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); + builder.setSeriesRecordingId(newSeriesRecording.getId()); + needUpdate = true; + } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { + // The new program belongs to the other series. + builder.setSeriesRecordingId(seriesRecording.getId()); + needUpdate = true; + seriesRecordingsToUpdate.add(seriesRecording); + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } else if (!Objects.equals(schedule.getSeasonNumber(), + program.getSeasonNumber()) + || !Objects.equals(schedule.getEpisodeNumber(), + program.getEpisodeNumber())) { + // The episode number has been changed. + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } + } else if (seriesRecordingForOldSchedule != null) { + // 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 + && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { - mDataManager.updateScheduledRecording( - builder.setStartTimeMs(program.getStartTimeUtcMillis()).build()); - } else if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis() + builder.setStartTimeMs(program.getStartTimeUtcMillis()); + needUpdate = true; + } + if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) @@ -179,27 +327,35 @@ class DvrDbSync { program.getThumbnailUri())) { mDataManager.updateScheduledRecording(builder.build()); } + if (!seriesRecordingsToUpdate.isEmpty()) { + // The series recordings will be updated after it's resumed. + mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); + } } } } - private class UpdateProgramTask extends AsyncQueryProgramTask { + private class QueryProgramTask extends AsyncQueryProgramTask { private final long mProgramId; - public UpdateProgramTask(long programId) { + QueryProgramTask(long programId) { super(mContext.getContentResolver(), programId); mProgramId = programId; } @Override protected void onCancelled(Program program) { - mUpdateProgramTask = null; + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } startNextUpdateIfNeeded(); } @Override protected void onPostExecute(Program program) { - mUpdateProgramTask = null; + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } handleUpdateProgram(program, mProgramId); startNextUpdateIfNeeded(); } diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 48ca6eee..5fa6f90f 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -17,29 +17,36 @@ package com.android.tv.dvr; import android.annotation.TargetApi; +import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; +import android.content.OperationApplicationException; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Handler; +import android.os.RemoteException; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; +import android.util.Range; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback; +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.util.AsyncDbTask; import com.android.tv.util.Utils; @@ -63,7 +70,6 @@ public class DvrManager { private static final boolean DEBUG = false; private final WritableDvrDataManager mDataManager; - private final ChannelDataManager mChannelDataManager; private final DvrScheduleManager mScheduleManager; // @GuardedBy("mListener") private final Map<Listener, Handler> mListener = new HashMap<>(); @@ -71,64 +77,124 @@ public class DvrManager { public DvrManager(Context context) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); + mAppContext = context.getApplicationContext(); ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); - mAppContext = context.getApplicationContext(); - mChannelDataManager = appSingletons.getChannelDataManager(); mScheduleManager = appSingletons.getDvrScheduleManager(); + if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms()); + } else { + // No need to handle DVR schedule load finished because schedule manager is initialized + // after the all the schedules are loaded. + if (!mDataManager.isRecordedProgramLoadFinished()) { + mDataManager.addRecordedProgramLoadFinishedListener( + new OnRecordedProgramLoadFinishedListener() { + @Override + public void onRecordedProgramLoadFinished() { + mDataManager.removeRecordedProgramLoadFinishedListener(this); + if (mDataManager.isInitialized() + && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded( + mDataManager.getRecordedPrograms()); + } + } + }); + } + if (!mScheduleManager.isInitialized()) { + mScheduleManager.addOnInitializeListener(new OnInitializeListener() { + @Override + public void onInitialize() { + mScheduleManager.removeOnInitializeListener(this); + if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded( + mDataManager.getRecordedPrograms()); + } + } + }); + } + } + mDataManager.addRecordedProgramListener(new RecordedProgramListener() { + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) { + return; + } + for (RecordedProgram recordedProgram : recordedPrograms) { + createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + // Removing series recording is handled in the SeriesRecordingDetailsFragment. + } + }); + } + + private void createSeriesRecordingsForRecordedProgramsIfNeeded( + List<RecordedProgram> recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); + } + } + + private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) { + if (recordedProgram.getSeriesId() != null) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(recordedProgram.getSeriesId()); + if (seriesRecording == null) { + addSeriesRecording(recordedProgram); + } + } } /** * Schedules a recording for {@code program}. */ - public void addSchedule(Program program) { + public ScheduledRecording addSchedule(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return; - } - TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program); - if (input == null) { - Log.e(TAG, "Can't find input for program: " + program); - return; + return null; } - ScheduledRecording schedule; SeriesRecording seriesRecording = getSeriesRecording(program); - if (seriesRecording == null) { - schedule = createScheduledRecordingBuilder(input.getId(), program) - .setPriority(mScheduleManager.suggestNewPriority()) - .build(); - } else { - schedule = createScheduledRecordingBuilder(input.getId(), program) - .setPriority(seriesRecording.getPriority()) - .setSeriesRecordingId(seriesRecording.getId()) - .build(); - } - mDataManager.addScheduledRecording(schedule); + return addSchedule(program, seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : seriesRecording.getPriority()); } /** - * Schedules a recording for {@code program} instead of the list of recording that conflict. - * - * @param program the program to record - * @param recordingsToOverride the possible empty list of recordings that will not be recorded + * Schedules a recording for {@code program} with the highest priority so that the schedule + * can be recorded. */ - public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) { - Log.i(TAG, "Adding scheduled recording of " + program + " instead of " + - recordingsToOverride); + public ScheduledRecording addScheduleWithHighestPriority(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return; + return null; } + SeriesRecording seriesRecording = getSeriesRecording(program); + return addSchedule(program, seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : mScheduleManager.suggestHighestPriority(seriesRecording.getInputId(), + new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()), + seriesRecording.getPriority())); + } + + private ScheduledRecording addSchedule(Program program, long priority) { TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program); if (input == null) { Log.e(TAG, "Can't find input for program: " + program); - return; + return null; } - Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR); - long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE - : recordingsToOverride.get(0).getPriority() + 1; - ScheduledRecording r = createScheduledRecordingBuilder(input.getId(), program) + ScheduledRecording schedule; + SeriesRecording seriesRecording = getSeriesRecording(program); + schedule = createScheduledRecordingBuilder(input.getId(), program) .setPriority(priority) + .setSeriesRecordingId(seriesRecording == null ? SeriesRecording.ID_NOT_SET + : seriesRecording.getId()) .build(); - mDataManager.addScheduledRecording(r); + mDataManager.addScheduledRecording(schedule); + return schedule; } /** @@ -148,6 +214,15 @@ public class DvrManager { addScheduleInternal(input.getId(), channel.getId(), startTime, endTime); } + /** + * Adds the schedule. + */ + public void addSchedule(ScheduledRecording schedule) { + if (mDataManager.isDvrScheduleLoadFinished()) { + mDataManager.addScheduledRecording(schedule); + } + } + private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) { mDataManager.addScheduledRecording(ScheduledRecording .builder(inputId, channelId, startTime, endTime) @@ -156,10 +231,10 @@ public class DvrManager { } /** - * Adds a new series recording and schedules for the programs. + * Adds a new series recording and schedules for the programs with the initial state. */ public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule) { + List<Program> programsToSchedule, @SeriesState int initialState) { Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { @@ -172,6 +247,7 @@ public class DvrManager { } SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram) .setPriority(mScheduleManager.suggestNewSeriesPriority()) + .setState(initialState) .build(); mDataManager.addSeriesRecording(seriesRecording); // The schedules for the recorded programs should be added not to create the schedule the @@ -181,6 +257,18 @@ public class DvrManager { return seriesRecording; } + private void addSeriesRecording(RecordedProgram recordedProgram) { + SeriesRecording seriesRecording = + SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram) + .setPriority(mScheduleManager.suggestNewSeriesPriority()) + .setState(SeriesRecording.STATE_SERIES_STOPPED) + .build(); + mDataManager.addSeriesRecording(seriesRecording); + // The schedules for the recorded programs should be added not to create the schedule the + // duplicate episodes. + addRecordedProgramToSeriesRecording(seriesRecording); + } + private void addRecordedProgramToSeriesRecording(SeriesRecording series) { List<ScheduledRecording> toAdd = new ArrayList<>(); for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) { @@ -221,11 +309,8 @@ public class DvrManager { mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { if (scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_CANCELED) { + == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) - .setPriority(series.getPriority()) .setSeriesRecordingId(series.getId()) .build(); if (!r.equals(scheduleWithSameProgram)) { @@ -233,15 +318,10 @@ public class DvrManager { } } } else { - ScheduledRecording.Builder scheduledRecordingBuilder = - createScheduledRecordingBuilder(input.getId(), program) + toAdd.add(createScheduledRecordingBuilder(input.getId(), program) .setPriority(series.getPriority()) - .setSeriesRecordingId(series.getId()); - if (series.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - scheduledRecordingBuilder.setState( - ScheduledRecording.STATE_RECORDING_CANCELED); - } - toAdd.add(scheduledRecordingBuilder.build()); + .setSeriesRecordingId(series.getId()) + .build()); } } if (!toAdd.isEmpty()) { @@ -257,29 +337,33 @@ public class DvrManager { */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - // TODO: revise this method. b/30946239 - boolean isPreviousCanceled = false; - long oldPriority = 0; + SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext); + scheduler.pauseUpdate(); SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); if (previousSeries != null) { - isPreviousCanceled = previousSeries.getState() - == SeriesRecording.STATE_SERIES_CANCELED; - oldPriority = previousSeries.getPriority(); + if (previousSeries.getChannelOption() != series.getChannelOption() + || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && previousSeries.getChannelId() != series.getChannelId())) { + List<ScheduledRecording> schedules = + mDataManager.getScheduledRecordings(series.getId()); + List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); + for (ScheduledRecording schedule : schedules) { + if (schedule.isNotStarted()) { + schedulesToRemove.add(schedule); + } + } + mDataManager.removeScheduledRecording(true, + ScheduledRecording.toArray(schedulesToRemove)); + } } mDataManager.updateSeriesRecording(series); - if (!isPreviousCanceled && series.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - cancelScheduleToSeriesRecording(series); - } else if (isPreviousCanceled - && series.getState() == SeriesRecording.STATE_SERIES_NORMAL) { - resumeScheduleToSeriesRecording(series); - } - if (oldPriority != series.getPriority()) { + if (previousSeries == null + || previousSeries.getPriority() != series.getPriority()) { long priority = series.getPriority(); List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(series.getId())) { - if (schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS - && schedule.getStartTimeMs() > System.currentTimeMillis()) { + if (schedule.isNotStarted()) { schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) .setPriority(priority).build()); } @@ -289,59 +373,10 @@ public class DvrManager { ScheduledRecording.toArray(schedulesToUpdate)); } } + scheduler.resumeUpdate(); } } - private void cancelScheduleToSeriesRecording(SeriesRecording series) { - List<ScheduledRecording> allRecordings = mDataManager.getAvailableScheduledRecordings(); - for (ScheduledRecording recording : allRecordings) { - if (recording.getSeriesRecordingId() == series.getId()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - stopRecording(recording); - continue; - } - updateScheduledRecording(ScheduledRecording.buildFrom(recording).setState - (ScheduledRecording.STATE_RECORDING_CANCELED).build()); - } - } - } - - private void resumeScheduleToSeriesRecording(SeriesRecording series) { - List<ScheduledRecording> allRecording = mDataManager - .getAvailableAndCanceledScheduledRecordings(); - for (ScheduledRecording recording : allRecording) { - if (recording.getSeriesRecordingId() == series.getId()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED && - recording.getEndTimeMs() > System.currentTimeMillis()) { - updateScheduledRecording(ScheduledRecording.buildFrom(recording) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED).build()); - } - } - } - } - - /** - * Queries the programs which belong to the same series as {@code seriesProgram}. - * <p> - * It's done in the background because it needs the DB access, and the callback will be called - * when it finishes. - */ - public void queryProgramsForSeries(Program seriesProgram, ProgramLoadCallback callback) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { - callback.onProgramLoadFinished(Collections.emptyList()); - return; - } - TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, seriesProgram); - if (input == null) { - Log.e(TAG, "Can't find input for program: " + seriesProgram); - return; - } - SeriesRecordingScheduler.getInstance(mAppContext).queryPrograms( - SeriesRecording.builder(input.getId(), seriesProgram) - .setPriority(mScheduleManager.suggestNewPriority()) - .build(), callback); - } - /** * Removes the series recording and all the corresponding schedules which are not started yet. */ @@ -365,6 +400,33 @@ 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) { @@ -401,6 +463,23 @@ public class DvrManager { } /** + * Removes scheduled recordings without changing to the DELETED state. + */ + public void forceRemoveScheduledRecording(ScheduledRecording... schedules) { + Log.i(TAG, "Force removing " + Arrays.asList(schedules)); + if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + return; + } + for (ScheduledRecording r : schedules) { + if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + stopRecording(r); + } else { + mDataManager.removeScheduledRecording(true, r); + } + } + } + + /** * Removes the recorded program. It deletes the file if possible. */ public void removeRecordedProgram(Uri recordedProgramUri) { @@ -434,51 +513,51 @@ public class DvrManager { @Override protected Void doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); - resolver.delete(recordedProgram.getUri(), null, null); - try { - Uri dataUri = recordedProgram.getDataUri(); - if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) - && dataUri.getPath() != null) { - File recordedProgramPath = new File(dataUri.getPath()); - if (!recordedProgramPath.exists()) { - if (DEBUG) Log.d(TAG, "File to delete not exist: " - + recordedProgramPath); - } else { - Utils.deleteDirOrFile(recordedProgramPath); - if (DEBUG) { - Log.d(TAG, "Sucessfully deleted files of the recorded program: " - + recordedProgram.getDataUri()); - } + int deletedCounts = resolver.delete(recordedProgram.getUri(), null, null); + if (deletedCounts > 0) { + // TODO: executeOnExecutor should be called on the main thread. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + removeRecordedData(recordedProgram.getDataUri()); + return null; } - } - } catch (SecurityException e) { - if (DEBUG) { - Log.d(TAG, "To delete " + recordedProgram - + "\nyou should manually delete video data at" - + "\nadb shell rm -rf " + recordedProgram.getDataUri()); - } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } return null; } }.executeOnDbThread(); } - /** - * Remove all recorded programs due to missing storage. - * - * @param inputId for the recorded programs to remove - */ - public void removeRecordedProgramByMissingStorage(final String inputId) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { - return; + public void removeRecordedPrograms(List<Long> recordedProgramIds) { + final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>(); + final List<Uri> dataUris = new ArrayList<>(); + for (Long rId : recordedProgramIds) { + RecordedProgram r = mDataManager.getRecordedProgram(rId); + if (r != null) { + dataUris.add(r.getDataUri()); + dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build()); + } } new AsyncDbTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); - String args[] = { inputId }; - resolver.delete(TvContract.RecordedPrograms.CONTENT_URI, - TvContract.RecordedPrograms.COLUMN_INPUT_ID + " = ?", args); + try { + resolver.applyBatch(TvContract.AUTHORITY, dbOperations); + // TODO: executeOnExecutor should be called on the main thread. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + for (Uri dataUri : dataUris) { + removeRecordedData(dataUri); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (RemoteException | OperationApplicationException e) { + Log.w(TAG, "Remove reocrded programs from DB failed.", e); + } return null; } }.executeOnDbThread(); @@ -527,10 +606,9 @@ public class DvrManager { * {@code false}. */ public boolean isConflicting(ScheduledRecording schedule) { - if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return false; - } - return mScheduleManager.isConflicting(schedule); + return schedule != null + && SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished()) + && mScheduleManager.isConflicting(schedule); } /** @@ -547,20 +625,26 @@ public class DvrManager { } /** - * Returns the earliest end time of the current recording for the TV input. If there are no - * recordings, Long.MAX_VALUE is returned. + * Sets the highest priority to the schedule. */ - public long getEarliestRecordingEndTime(String inputId) { - long result = Long.MAX_VALUE; - for (ScheduledRecording schedule : mDataManager.getStartedRecordings()) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, - schedule.getChannelId()); - if (input != null && input.getId().equals(inputId) - && schedule.getEndTimeMs() < result) { - result = schedule.getEndTimeMs(); + public void setHighestPriority(ScheduledRecording schedule) { + if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + long newPriority = mScheduleManager.suggestHighestPriority(schedule); + if (newPriority != schedule.getPriority()) { + mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) + .setPriority(newPriority).build()); } } - return result; + } + + /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(ScheduledRecording schedule) { + if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + return mScheduleManager.suggestHighestPriority(schedule); + } + return DvrScheduleManager.DEFAULT_PRIORITY; } /** @@ -622,6 +706,23 @@ public class DvrManager { } /** + * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to + * the series recording {@code seriesRecordingId}. + */ + public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) { + if (!mDataManager.isDvrScheduleLoadFinished()) { + return Collections.emptyList(); + } + List<ScheduledRecording> schedules = new ArrayList<>(); + for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) { + if (schedule.isInProgress() || schedule.isNotStarted()) { + schedules.add(schedule); + } + } + return schedules; + } + + /** * Returns the series recording related to the program. */ @Nullable @@ -704,6 +805,40 @@ public class DvrManager { return null; } + @WorkerThread + private void removeRecordedData(Uri dataUri) { + try { + if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) + && dataUri.getPath() != null) { + File recordedProgramPath = new File(dataUri.getPath()); + if (!recordedProgramPath.exists()) { + if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath); + } else { + Utils.deleteDirOrFile(recordedProgramPath); + if (DEBUG) { + Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri); + } + } + } + } catch (SecurityException e) { + if (DEBUG) { + Log.d(TAG, "To delete this recorded program, please manually delete video data at" + + "\nadb shell rm -rf " + dataUri); + } + } + } + + /** + * Remove all the records related to the input. + * <p> + * Note that this should be called after the input was removed. + */ + public void forgetStorage(String inputId) { + if (mDataManager.isInitialized()) { + mDataManager.forgetStorage(inputId); + } + } + /** * Listener internally used inside dvr package. */ diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java index 3320e0fd..5deda44a 100644 --- a/src/com/android/tv/dvr/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/DvrPlaybackActivity.java @@ -23,6 +23,7 @@ import android.os.Bundle; import android.util.Log; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; /** @@ -36,6 +37,7 @@ public class DvrPlaybackActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_playback); diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java index da815712..9759a856 100644 --- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java @@ -17,6 +17,7 @@ package com.android.tv.dvr; import android.app.Activity; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadata; @@ -32,8 +33,10 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; +import com.android.tv.util.Utils; public class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; @@ -50,8 +53,8 @@ public class DvrPlaybackMediaSessionHelper { private final DvrWatchedPositionManager mDvrWatchedPositionManager; private final ChannelDataManager mChannelDataManager; - public DvrPlaybackMediaSessionHelper(Activity activity, - String mediaSessionTag, DvrPlayer dvrPlayer) { + public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, + DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { mActivity = activity; mDvrPlayer = dvrPlayer; mDvrWatchedPositionManager = @@ -71,6 +74,21 @@ public class DvrPlaybackMediaSessionHelper { .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); } } + + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } }); initializeMediaSession(mediaSessionTag); } @@ -122,7 +140,7 @@ public class DvrPlaybackMediaSessionHelper { * Checks if the recorded program is the same as now playing one. */ public boolean isCurrentProgram(RecordedProgram program) { - return program == null ? false : program.equals(getProgram()); + return program != null && program.equals(getProgram()); } /** @@ -216,7 +234,7 @@ public class DvrPlaybackMediaSessionHelper { 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> () { + new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... arg0) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); @@ -252,16 +270,23 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onPlay() { - mDvrPlayer.play(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.play(); + } } @Override public void onPause() { - mDvrPlayer.pause(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.pause(); + } } @Override public void onFastForward() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { mSpeedLevel++; @@ -277,6 +302,9 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onRewind() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { mSpeedLevel++; @@ -291,7 +319,9 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onSeekTo(long positionMs) { - mDvrPlayer.seekTo(positionMs); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.seekTo(positionMs); + } } } } diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java index 027d99f4..5656655c 100644 --- a/src/com/android/tv/dvr/DvrPlayer.java +++ b/src/com/android/tv/dvr/DvrPlayer.java @@ -55,6 +55,8 @@ public class DvrPlayer { private boolean mPauseOnPrepared; private final PlaybackParams mPlaybackParams = new PlaybackParams(); private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); + private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + private boolean mTimeShiftPlayAvailable; public static class DvrPlayerCallback { /** @@ -67,6 +69,10 @@ public class DvrPlayer { * Called when the playback state or the playback speed is changed. */ public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } + /** + * Called when the playback toward the end. + */ + public void onPlaybackEnded() { } } public interface AspectRatioChangedListener { @@ -208,7 +214,7 @@ public class DvrPlayer { } positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); - mTvView.timeShiftSeekTo(positionMs); + mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || mPlaybackState == PlaybackState.STATE_REWINDING) { mPlaybackState = PlaybackState.STATE_PLAYING; @@ -222,12 +228,14 @@ public class DvrPlayer { */ public void reset() { if (DEBUG) Log.d(TAG, "reset()"); - mTvView.reset(); + mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); mPlaybackState = PlaybackState.STATE_NONE; + mTvView.reset(); + mTimeShiftPlayAvailable = false; + mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; mTimeShiftCurrentPositionMs = 0; mPlaybackParams.setSpeed(1.0f); mProgram = null; - mCallback.onPlaybackStateChanged(mPlaybackState, 1); } /** @@ -317,43 +325,50 @@ public class DvrPlayer { private void setTvViewCallbacks() { mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } + + @Override public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { - // Workaround solution for b/29994826: - // prevents rewinding and fast-forwarding over the ends. + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of playback + // on the first time we get current position, and the start position should be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; if (mPlaybackState == PlaybackState.STATE_REWINDING && timeMs <= REWIND_POSITION_MARGIN_MS) { play(); - } else if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING - && timeMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { - mTvView.timeShiftSeekTo(mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS); - pause(); - } - else { + } else { mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } } } }); mTvView.setCallback(new TvView.TvInputCallback() { @Override public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged"); + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE && mPlaybackState == PlaybackState.STATE_CONNECTING) { - if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mTvView.timeShiftSeekTo(getRealSeekPosition( - mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS)); - mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - } - if (mPauseOnPrepared) { - mTvView.timeShiftPause(); - mPlaybackState = PlaybackState.STATE_PAUSED; - mPauseOnPrepared = false; - } else { - mTvView.timeShiftResume(); - mPlaybackState = PlaybackState.STATE_PLAYING; - } - mCallback.onPlaybackStateChanged(mPlaybackState, 1); + mTimeShiftPlayAvailable = true; } } @@ -390,4 +405,21 @@ public class DvrPlayer { } }); } + + private void resumeToWatchedPositionIfNeeded() { + if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, + SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + if (mPauseOnPrepared) { + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + mPauseOnPrepared = false; + } else { + mTvView.timeShiftResume(); + mPlaybackState = PlaybackState.STATE_PLAYING; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java index 39be7961..8c40aaa8 100644 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ b/src/com/android/tv/dvr/DvrRecordingService.java @@ -70,7 +70,6 @@ public class DvrRecordingService extends Service { super.onCreate(); SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); ApplicationSingletons singletons = TvApplication.getSingletons(this); - DvrManager dvrManager = singletons.getDvrManager(); WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); @@ -103,7 +102,7 @@ public class DvrRecordingService extends Service { mScheduler.stop(); mScheduler = null; if (mHandlerThread != null) { - mHandlerThread.quit(); + mHandlerThread.quitSafely(); mHandlerThread = null; } super.onDestroy(); diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index aa77c400..a5851a75 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,6 +24,7 @@ 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; @@ -34,16 +35,18 @@ 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.util.CompositeComparator; import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; /** * A class to manage the schedules. @@ -64,15 +67,34 @@ public class DvrScheduleManager { // The new priority will have the offset from the existing one. private static final long PRIORITY_OFFSET = 1024; + private static final Comparator<ScheduledRecording> RESULT_COMPARATOR = + new CompositeComparator<>( + ScheduledRecording.PRIORITY_COMPARATOR.reversed(), + ScheduledRecording.START_TIME_COMPARATOR, + ScheduledRecording.ID_COMPARATOR.reversed()); + + // The candidate comparator should be the consistent with + // InputTaskScheduler#CANDIDATE_COMPARATOR. + private static final Comparator<ScheduledRecording> CANDIDATE_COMPARATOR = + new CompositeComparator<>( + ScheduledRecording.PRIORITY_COMPARATOR, + ScheduledRecording.END_TIME_COMPARATOR, + ScheduledRecording.ID_COMPARATOR); + private final Context mContext; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); - private final Map<String, List<ScheduledRecording>> mInputConflictMap = 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<>(); private boolean mInitialized; + private final Set<OnInitializeListener> mOnInitializeListeners = new CopyOnWriteArraySet<>(); private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>(); private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners = new ArraySet<>(); @@ -106,10 +128,13 @@ public class DvrScheduleManager { if (!schedule.isNotStarted() && !schedule.isInProgress()) { continue; } - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - schedule.getChannelId()); - if (input == null) { + TvInputInfo input = Utils + .getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument(input != null, TAG, + "Input was removed for : " + schedule)) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -131,9 +156,11 @@ public class DvrScheduleManager { } for (ScheduledRecording schedule : scheduledRecordings) { TvInputInfo input = Utils - .getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + .getTvInputInfoForInputId(mContext, schedule.getInputId()); if (input == null) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -144,6 +171,14 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } + Map<ScheduledRecording, Boolean> conflictInfo = + mInputConflictInfoMap.get(inputId); + if (conflictInfo != null) { + conflictInfo.remove(schedule); + if (conflictInfo.isEmpty()) { + mInputConflictInfoMap.remove(inputId); + } + } } onSchedulesChanged(); notifyScheduledRecordingRemoved(scheduledRecordings); @@ -157,9 +192,12 @@ public class DvrScheduleManager { } for (ScheduledRecording schedule : scheduledRecordings) { TvInputInfo input = Utils - .getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { + .getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument(input != null, TAG, + "Input was removed for : " + schedule)) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -170,8 +208,7 @@ public class DvrScheduleManager { } // Compare ID because ScheduledRecording.equals() doesn't work if the state // is changed. - Iterator<ScheduledRecording> i = schedules.iterator(); - while (i.hasNext()) { + for (Iterator<ScheduledRecording> i = schedules.iterator(); i.hasNext(); ) { if (i.next().getId() == schedule.getId()) { i.remove(); break; @@ -183,6 +220,24 @@ public class DvrScheduleManager { if (schedules.isEmpty()) { mInputScheduleMap.remove(inputId); } + // Update conflict list as well + Map<ScheduledRecording, Boolean> 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); + } + } } onSchedulesChanged(); notifyScheduledRecordingStatusChanged(scheduledRecordings); @@ -249,33 +304,39 @@ public class DvrScheduleManager { schedules.add(schedule); } } - mInitialized = true; + if (!mInitialized) { + mInitialized = true; + notifyInitialize(); + } onSchedulesChanged(); } private void onSchedulesChanged() { + // TODO: notify conflict state change when some conflicting recording becomes partially + // conflicting, vice versa. List<ScheduledRecording> addedConflicts = new ArrayList<>(); List<ScheduledRecording> removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - List<ScheduledRecording> oldConflicts = mInputConflictMap.get(inputId); + Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); - if (oldConflicts != null) { - for (ScheduledRecording r : oldConflicts) { + if (oldConflictsInfo != null) { + for (ScheduledRecording r : oldConflictsInfo.keySet()) { oldConflictMap.put(r.getId(), r); } } - List<ScheduledRecording> conflicts = getConflictingSchedules(inputId); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); + if (conflictInfo.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); + } } } removedConflicts.addAll(oldConflictMap.values()); - if (conflicts.isEmpty()) { - mInputConflictMap.remove(inputId); - } else { - mInputConflictMap.put(inputId, conflicts); - } } if (!removedConflicts.isEmpty()) { notifyConflictStateChange(false, ScheduledRecording.toArray(removedConflicts)); @@ -334,6 +395,29 @@ public class DvrScheduleManager { } /** + * Adds a {@link OnInitializeListener}. + */ + public final void addOnInitializeListener(OnInitializeListener listener) { + mOnInitializeListeners.add(listener); + } + + /** + * Removes a {@link OnInitializeListener}. + */ + public final void removeOnInitializeListener(OnInitializeListener listener) { + mOnInitializeListeners.remove(listener); + } + + /** + * Calls {@link OnInitializeListener#onInitialize} for each listener. + */ + private void notifyInitialize() { + for (OnInitializeListener l : mOnInitializeListeners) { + l.onInitialize(); + } + } + + /** * Adds a {@link OnConflictStateChangeListener}. */ public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) { @@ -380,6 +464,47 @@ public class DvrScheduleManager { } /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(ScheduledRecording schedule) { + List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId()); + if (schedules == null) { + return DEFAULT_PRIORITY; + } + long highestPriority = Long.MIN_VALUE; + for (ScheduledRecording r : schedules) { + if (!r.equals(schedule) && r.isOverLapping(schedule) + && r.getPriority() > highestPriority) { + highestPriority = r.getPriority(); + } + } + if (highestPriority == Long.MIN_VALUE || highestPriority < schedule.getPriority()) { + return schedule.getPriority(); + } + return highestPriority + PRIORITY_OFFSET; + } + + /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) { + List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); + if (schedules == null) { + return DEFAULT_PRIORITY; + } + long highestPriority = Long.MIN_VALUE; + for (ScheduledRecording r : schedules) { + if (r.isOverLapping(peroid) && r.getPriority() > highestPriority) { + highestPriority = r.getPriority(); + } + } + if (highestPriority == Long.MIN_VALUE || highestPriority < basePriority) { + return basePriority; + } + return highestPriority + PRIORITY_OFFSET; + } + + /** * Returns the priority for a series recording. * <p> * The recording will have the higher priority than the existing series. @@ -411,11 +536,12 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this program is. + * Returns a sorted list of all scheduled recordings that will not be recorded if + * this program is going to be recorded, with their priorities in decending order. * <p> - * Any empty list means there is no conflicts. If there is conflict the program must be - * scheduled to record with a priority higher than the first recording in the list returned. + * An empty list means there is no conflicts. If there is conflict, a priority higher than + * the first recording in the returned list should be assigned to the new schedule of this + * program to guarantee the program would be completely recorded. */ public List<ScheduledRecording> getConflictingSchedules(Program program) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); @@ -439,11 +565,34 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this channel is. + * Returns list of all conflicting scheduled recordings with schedules belonging to {@code + * seriesRecording} + * recording. + * <p> + * Any empty list means there is no conflicts. + */ + public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) { + SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); + SoftPreconditions.checkState(seriesRecording != null, TAG, "series recording is null"); + if (!mInitialized || seriesRecording == null) { + return Collections.emptyList(); + } + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, seriesRecording.getInputId()); + if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { + return Collections.emptyList(); + } + List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( + seriesRecording.getId()); + return getConflictingSchedules(input, schedulesForSeries); + } + + /** + * Returns a sorted list of all scheduled recordings that will not be recorded if + * this channel is going to be recorded, with their priority in decending order. * <p> - * Any empty list means there is no conflicts. If there is conflict the channel must be - * scheduled to record with a priority higher than the first recording in the list returned. + * An empty list means there is no conflicts. If there is conflict, a priority higher than + * the first recording in the returned list should be assigned to the new schedule of this + * channel to guarantee the channel would be completely recorded in the designated time range. */ public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs, long endTimeMs) { @@ -468,18 +617,18 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private List<ScheduledRecording> getConflictingSchedules(String inputId) { + private Map<ScheduledRecording, Boolean> 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.emptyList(); + return Collections.emptyMap(); } List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyList(); + return Collections.emptyMap(); } - return getConflictingSchedules(schedules, input.getTunerCount()); + return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } /** @@ -490,14 +639,33 @@ public class DvrScheduleManager { */ public boolean isConflicting(ScheduledRecording schedule) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " + + schedule.getChannelId()); + if (!mInitialized || input == null) { + return false; + } + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule); + } + + /** + * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be + * recorded even if the priority of the schedule is not raised. + * <p> + * If the given schedule is not conflicting or is totally conflicting, i.e., cannot be recorded + * at all, this method returns {@code false} in both cases. + */ + public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) { + SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId()); if (!mInitialized || input == null) { return false; } - List<ScheduledRecording> conflicts = mInputConflictMap.get(input.getId()); - return conflicts != null && conflicts.contains(schedule); + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.getOrDefault(schedule, false); } /** @@ -599,7 +767,7 @@ public class DvrScheduleManager { List<ScheduledRecording> result = new ArrayList<>(); result.addAll(getConflictingSchedules(schedulesSameChannel, 1)); result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount)); - Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR); + Collections.sort(result, RESULT_COMPARATOR); return result; } @@ -639,66 +807,161 @@ public class DvrScheduleManager { */ public static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount) { - return getConflictingSchedules(schedules, tunerCount, - Collections.singletonList(new Range<>(Long.MIN_VALUE, Long.MAX_VALUE))); + return getConflictingSchedules(schedules, tunerCount, null); } @VisibleForTesting - static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedules, - int tunerCount, List<Range<Long>> periods) { - List<ScheduledRecording> schedulesToCheck = new ArrayList<>(); - // Filter out non-overlapping or empty duration of schedules. - for (ScheduledRecording schedule : schedules) { - for (Range<Long> period : periods) { - if (schedule.isOverLapping(period) - && schedule.getStartTimeMs() < schedule.getEndTimeMs()) { - schedulesToCheck.add(schedule); - break; + 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); + return result; + } + + @VisibleForTesting + static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + List<ScheduledRecording> schedules, int tunerCount) { + return getConflictingSchedulesInfo(schedules, tunerCount, null); + } + + /** + * This is the core method to calculate all the conflicting schedules (in given periods). + * <p> + * Note that this method will ignore duplicated schedules with a same hash code. (Please refer + * to {@link ScheduledRecording#hashCode}.) + * + * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean + * value denotes if the scheduled recording is partially conflicting, i.e., is possible + * to be partially recorded under the given schedules and tuner count {@code true}, + * or not {@code false}. + */ + private static Map<ScheduledRecording, Boolean> 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, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); + // Simulate InputTaskScheduler. + while (!schedulesToCheck.isEmpty()) { + ScheduledRecording schedule = schedulesToCheck.remove(0); + removeFinishedRecordings(recordings, schedule.getStartTimeMs()); + if (recordings.size() < tunerCount) { + 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); } - } - } - // Sort by the end time. - // If a.end <= b.end <= c.end and a overlaps with b and c, then b overlaps with c. - // Likewise, if a1.end <= a2.end <= ... , all the schedules which overlap with a1 overlap - // with each other. - Collections.sort(schedulesToCheck, ScheduledRecording.END_TIME_COMPARATOR); - Set<ScheduledRecording> conflicts = new ArraySet<>(); - List<ScheduledRecording> overlaps = new ArrayList<>(); - for (int i = 0; i < schedulesToCheck.size(); ++i) { - ScheduledRecording r1 = schedulesToCheck.get(i); - if (conflicts.contains(r1)) { - // No need to check r1 because it's a conflicting schedule already. - continue; - } - overlaps.clear(); - overlaps.add(r1); - // Find schedules which overlap with r1. - for (int j = i + 1; j < schedulesToCheck.size(); ++j) { - ScheduledRecording r2 = schedulesToCheck.get(j); - if (!conflicts.contains(r2) && r1.getEndTimeMs() > r2.getStartTimeMs()) { - overlaps.add(r2); + } else { + ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); + if (candidate != null) { + if (!modified2OriginalSchedules.containsKey(candidate)) { + conflicts.put(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); + } + } else { + if (!modified2OriginalSchedules.containsKey(schedule)) { + // if schedule has been modified, it's already conflicted. + // No need to add it again. + conflicts.put(schedule, false); + } + long earliestEndTime = getEarliestEndTime(recordings); + if (earliestEndTime < schedule.getEndTimeMs()) { + // The schedule can starts when other recording ends even though it's + // clipped. + ScheduledRecording modifiedSchedule = ScheduledRecording.buildFrom(schedule) + .setStartTimeMs(earliestEndTime).build(); + ScheduledRecording originalSchedule = + modified2OriginalSchedules.getOrDefault(schedule, schedule); + modified2OriginalSchedules.put(modifiedSchedule, originalSchedule); + int insertPosition = Collections.binarySearch(schedulesToCheck, + modifiedSchedule, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + if (insertPosition >= 0) { + schedulesToCheck.add(insertPosition, modifiedSchedule); + } else { + schedulesToCheck.add(-insertPosition - 1, modifiedSchedule); + } + } } } - Collections.sort(overlaps, ScheduledRecording.PRIORITY_COMPARATOR); - // If there are more than one overlapping schedules for the same channel, only one - // schedule will be recorded. - HashSet<Long> channelIds = new HashSet<>(); - for (Iterator<ScheduledRecording> iter = overlaps.iterator(); iter.hasNext(); ) { + } + // Returns only the schedules with the given range. + if (periods != null && !periods.isEmpty()) { + for (Iterator<ScheduledRecording> iter = conflicts.keySet().iterator(); + iter.hasNext(); ) { + boolean overlapping = false; ScheduledRecording schedule = iter.next(); - if (channelIds.contains(schedule.getChannelId())) { - conflicts.add(schedule); + for (Range<Long> period : periods) { + if (schedule.isOverLapping(period)) { + overlapping = true; + break; + } + } + if (!overlapping) { iter.remove(); - } else { - channelIds.add(schedule.getChannelId()); } } - if (overlaps.size() > tunerCount) { - conflicts.addAll(overlaps.subList(tunerCount, overlaps.size())); + } + return conflicts; + } + + private static void removeFinishedRecordings(List<ScheduledRecording> recordings, + long currentTimeMs) { + for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) { + if (iter.next().getEndTimeMs() <= currentTimeMs) { + iter.remove(); } } - List<ScheduledRecording> result = new ArrayList<>(conflicts); - Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR); - return result; + } + + /** + * @see InputTaskScheduler#getReplacableTask + */ + private static ScheduledRecording findReplaceableRecording(List<ScheduledRecording> recordings, + ScheduledRecording schedule) { + // Returns the recording with the following priority. + // 1. The recording with the lowest priority is returned. + // 2. If the priorities are the same, the recording which finishes early is returned. + // 3. If 1) and 2) are the same, the early created schedule is returned. + ScheduledRecording candidate = null; + for (ScheduledRecording recording : recordings) { + if (schedule.getPriority() > recording.getPriority()) { + if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) { + candidate = recording; + } + } + } + return candidate; + } + + private static long getEarliestEndTime(List<ScheduledRecording> recordings) { + long earliest = Long.MAX_VALUE; + for (ScheduledRecording recording : recordings) { + if (earliest > recording.getEndTimeMs()) { + earliest = recording.getEndTimeMs(); + } + } + return earliest; + } + + /** + * A listener which is notified the initialization of schedule manager. + */ + public interface OnInitializeListener { + /** + * Called when the schedule manager has been initialized. + */ + void onInitialize(); } /** @@ -714,4 +977,4 @@ public class DvrScheduleManager { */ void onConflictStateChange(boolean conflict, ScheduledRecording... schedules); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java new file mode 100644 index 00000000..a653b5f4 --- /dev/null +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -0,0 +1,376 @@ +/* + * 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; + +import android.content.BroadcastReceiver; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.os.Looper; +import android.os.RemoteException; +import android.os.StatFs; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.util.Utils; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Signals DVR storage status change such as plugging/unplugging. + */ +public class DvrStorageStatusManager { + private static final String TAG = "DvrStorageStatusManager"; + private static final boolean DEBUG = false; + + /** + * Minimum storage size to support DVR + */ + public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB + private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES + = 10 * 1024 * 1024 * 1024L; // 10GB + private static final String RECORDING_DATA_SUB_PATH = "/recording"; + + private static final String[] PROJECTION = { + TvContract.RecordedPrograms._ID, + TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, + TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI + }; + private final static int BATCH_OPERATION_COUNT = 100; + + @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL, + STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING}) + @Retention(RetentionPolicy.SOURCE) + public @interface StorageStatus { + } + + /** + * Current storage is OK to record a program. + */ + public static final int STORAGE_STATUS_OK = 0; + + /** + * Current storage's total capacity is smaller than DVR requirement. + */ + public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1; + + /** + * Current storage's free space is insufficient to record programs. + */ + public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2; + + /** + * Current storage is missing. + */ + public static final int STORAGE_STATUS_MISSING = 3; + + private final Context mContext; + private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners = + new CopyOnWriteArraySet<>(); + private final boolean mRunningInMainProcess; + private MountedStorageStatus mMountedStorageStatus; + private boolean mStorageValid; + private CleanUpDbTask mCleanUpDbTask; + + private class MountedStorageStatus { + private final boolean mStorageMounted; + private final File mStorageMountedDir; + private final long mStorageMountedCapacity; + + private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) { + mStorageMounted = mounted; + mStorageMountedDir = mountedDir; + mStorageMountedCapacity = capacity; + } + + private boolean isValidForDvr() { + return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MountedStorageStatus)) { + return false; + } + MountedStorageStatus status = (MountedStorageStatus) other; + return mStorageMounted == status.mStorageMounted + && Objects.equals(mStorageMountedDir, status.mStorageMountedDir) + && mStorageMountedCapacity == status.mStorageMountedCapacity; + } + } + + public interface OnStorageMountChangedListener { + + /** + * Listener for DVR storage status change. + * + * @param storageMounted {@code true} when DVR possible storage is mounted, + * {@code false} otherwise. + */ + void onStorageMountChanged(boolean storageMounted); + } + + private final class StorageStatusBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + MountedStorageStatus result = getStorageStatusInternal(); + if (mMountedStorageStatus.equals(result)) { + return; + } + mMountedStorageStatus = result; + if (result.mStorageMounted && mRunningInMainProcess) { + // Cleans up DB in LC process. + // Tuner process is not always on. + if (mCleanUpDbTask != null) { + mCleanUpDbTask.cancel(true); + } + mCleanUpDbTask = new CleanUpDbTask(); + mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + boolean valid = result.isValidForDvr(); + if (valid == mStorageValid) { + return; + } + mStorageValid = valid; + for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) { + l.onStorageMountChanged(valid); + } + } + } + + /** + * Creates DvrStorageStatusManager. + * + * @param context {@link Context} + */ + public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) { + mContext = context; + mRunningInMainProcess = runningInMainProcess; + mMountedStorageStatus = getStorageStatusInternal(); + mStorageValid = mMountedStorageStatus.isValidForDvr(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + filter.addAction(Intent.ACTION_MEDIA_REMOVED); + filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + filter.addDataScheme(ContentResolver.SCHEME_FILE); + mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter); + } + + /** + * Adds the listener for receiving storage status change. + * + * @param listener + */ + public void addListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.add(listener); + } + + /** + * Removes the current listener. + */ + public void removeListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.remove(listener); + } + + /** + * Returns true if a storage is mounted. + */ + public boolean isStorageMounted() { + return mMountedStorageStatus.mStorageMounted; + } + + /** + * Returns the path to DVR recording data directory. + * This can take for a while sometimes. + */ + @WorkerThread + public File getRecordingRootDataDirectory() { + SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); + if (mMountedStorageStatus.mStorageMountedDir == null) { + return null; + } + File root = mContext.getExternalFilesDir(null); + String rootPath; + try { + rootPath = root != null ? root.getCanonicalPath() : null; + } catch (IOException | SecurityException e) { + return null; + } + return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH); + } + + /** + * Returns the current storage status for DVR recordings. + * + * @return {@link StorageStatus} + */ + @AnyThread + public @StorageStatus int getDvrStorageStatus() { + MountedStorageStatus status = mMountedStorageStatus; + if (status.mStorageMountedDir == null) { + return STORAGE_STATUS_MISSING; + } + if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) { + return STORAGE_STATUS_OK; + } + if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL; + } + try { + StatFs statFs = new StatFs(status.mStorageMountedDir.toString()); + if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + } catch (IllegalArgumentException e) { + // In rare cases, storage status change was not notified yet. + SoftPreconditions.checkState(false); + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + return STORAGE_STATUS_OK; + } + + /** + * Returns whether the storage has sufficient storage. + * + * @return {@code true} when there is sufficient storage, {@code false} otherwise + */ + public boolean isStorageSufficient() { + return getDvrStorageStatus() == STORAGE_STATUS_OK; + } + + private MountedStorageStatus getStorageStatusInternal() { + boolean storageMounted = + Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null; + storageMounted = storageMounted && storageMountedDir != null; + long storageMountedCapacity = 0L; + if (storageMounted) { + try { + StatFs statFs = new StatFs(storageMountedDir.toString()); + storageMountedCapacity = statFs.getTotalBytes(); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Storage mount status was changed."); + storageMounted = false; + storageMountedDir = null; + } + } + return new MountedStorageStatus( + storageMounted, storageMountedDir, storageMountedCapacity); + } + + private class CleanUpDbTask extends AsyncTask<Void, Void, Void> { + private final ContentResolver mContentResolver; + + private CleanUpDbTask() { + mContentResolver = mContext.getContentResolver(); + } + + @Override + protected Void 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 (ops == null || ops.isEmpty()) { + return null; + } + Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : " + + ops.size()); + for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) { + int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size() + ? ops.size() : (i + BATCH_OPERATION_COUNT); + ArrayList<ContentProviderOperation> batchOps = + new ArrayList<>(ops.subList(i, toIndex)); + try { + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Failed to clean up RecordedPrograms.", e); + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (mCleanUpDbTask == this) { + mCleanUpDbTask = null; + } + } + + private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) { + List<ContentProviderOperation> ops = new ArrayList<>(); + + try (Cursor c = mContentResolver.query( + TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) { + if (c == null) { + return null; + } + while (c.moveToNext()) { + @DvrStorageStatusManager.StorageStatus int storageStatus = + getDvrStorageStatus(); + if (isCancelled() + || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + ops.clear(); + break; + } + String id = c.getString(0); + String packageName = c.getString(1); + String dataUriString = c.getString(2); + if (dataUriString == null) { + continue; + } + Uri dataUri = Uri.parse(dataUriString); + if (!Utils.isInBundledPackageSet(packageName) + || dataUri == null || dataUri.getPath() == null + || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { + continue; + } + File recordedProgramDir = new File(dataUri.getPath()); + if (deleteAll || !recordedProgramDir.exists()) { + ops.add(ContentProviderOperation.newDelete( + TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); + } + } + return ops; + } + } + } +} diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java index be934fd4..c0d3b0c5 100644 --- a/src/com/android/tv/dvr/DvrUiHelper.java +++ b/src/com/android/tv/dvr/DvrUiHelper.java @@ -36,7 +36,6 @@ 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.DvrCancelAllSeriesRecordingDialogFragment; import com.android.tv.dvr.ui.DvrDetailsActivity; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; @@ -44,13 +43,19 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialo 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.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.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; 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.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import com.android.tv.util.Utils; @@ -82,7 +87,7 @@ public class DvrUiHelper { } } else { SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null) { + if (seriesRecording == null || seriesRecording.isStopped()) { DvrUiHelper.showScheduleDialog(activity, program); return false; } else { @@ -111,6 +116,32 @@ public class DvrUiHelper { } /** + * 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}. + */ + 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; + } + } + + /** * Shows the schedule dialog. */ public static void showScheduleDialog(MainActivity activity, Program program) { @@ -170,7 +201,7 @@ public class DvrUiHelper { /** * Shows DVR missing storage error dialog. */ - public static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { + private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId)); Bundle args = new Bundle(); args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId); @@ -178,15 +209,23 @@ public class DvrUiHelper { } /** + * Shows DVR small sized storage error dialog. + */ + public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); + } + + /** * Shows stop recording dialog. */ - public static void showStopRecordingDialog(MainActivity activity, Channel channel) { - if (channel == null) { - return; - } + public static void showStopRecordingDialog(Activity activity, long channelId, int reason, + HalfSizedDialogFragment.OnActionClickListener listener) { Bundle args = new Bundle(); - args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); - showDialogFragment(activity, new DvrStopRecordingDialogFragment(), args); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); + args.putInt(DvrStopRecordingFragment.KEY_REASON, reason); + DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment(); + fragment.setOnActionClickListener(listener); + showDialogFragment(activity, fragment, args); } /** @@ -244,7 +283,8 @@ public class DvrUiHelper { recordings) { ScheduledRecording earlistScheduledRecording = null; if (!recordings.isEmpty()) { - Collections.sort(recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + Collections.sort(recordings, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); earlistScheduledRecording = recordings.get(0); } return earlistScheduledRecording; @@ -300,32 +340,72 @@ public class DvrUiHelper { /** * Shows the series settings activity. + * + * @param channelIds Channel ID list which has programs belonging to the series. */ - public static void startSeriesSettingsActivity(Context context, long seriesRecordingId) { + public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, + @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); + intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); + 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); context.startActivity(intent); } /** - * Shows the details activity for the schedule. + * Shows "series recording scheduled" dialog activity. */ - public static void startDetailsActivity(Activity activity, ScheduledRecording schedule, + public static void StartSeriesScheduledDialogActivity(Context context, + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { + if (seriesRecording == null) { + return; + } + Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); + intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, + seriesRecording.getId()); + intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, + showViewScheduleOptionInDialog); + context.startActivity(intent); + } + + /** + * Shows the details activity for the DVR items. The type of DVR items may be + * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. + */ + public static void startDetailsActivity(Activity activity, Object dvrItem, @Nullable ImageView imageView, boolean hideViewSchedule) { - if (schedule == null) { + if (dvrItem == null) { return; } + Intent intent = new Intent(activity, DvrDetailsActivity.class); + long recordingId; int viewType; - if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; - } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + if (dvrItem instanceof ScheduledRecording) { + ScheduledRecording schedule = (ScheduledRecording) dvrItem; + recordingId = schedule.getId(); + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + } else { + return; + } + } else if (dvrItem instanceof RecordedProgram) { + recordingId = ((RecordedProgram) dvrItem).getId(); + viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + } else if (dvrItem instanceof SeriesRecording) { + recordingId = ((SeriesRecording) dvrItem).getId(); + viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; } else { return; } - Intent intent = new Intent(activity, DvrDetailsActivity.class); + intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, schedule.getId()); intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); Bundle bundle = null; if (imageView != null) { @@ -336,30 +416,18 @@ public class DvrUiHelper { } /** - * Shows the details activity for the recorded program. - */ - public static void startDetailsActivity(Activity activity, RecordedProgram recordedProgram, - @Nullable ImageView imageView) { - Intent intent = new Intent(activity, DvrDetailsActivity.class); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordedProgram.getId()); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, - DvrDetailsActivity.RECORDED_PROGRAM_VIEW); - Bundle bundle = null; - if (imageView != null) { - bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); - } - activity.startActivity(intent, bundle); - } - - /** * Shows the cancel all dialog for series schedules list. */ - public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity) { - DvrCancelAllSeriesRecordingDialogFragment dvrCancelAllSeriesRecordingDialogFragment = - new DvrCancelAllSeriesRecordingDialogFragment(); - dvrCancelAllSeriesRecordingDialogFragment.show(activity.getFragmentManager(), - DvrCancelAllSeriesRecordingDialogFragment.DIALOG_TAG); + public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, + SeriesRecording seriesRecording) { + DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = + new DvrStopSeriesRecordingDialogFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, + seriesRecording); + dvrStopSeriesRecordingDialogFragment.setArguments(arguments); + dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), + DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); } /** diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index cb723f83..4eada742 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -19,9 +19,12 @@ package com.android.tv.dvr; import android.content.Context; import android.content.SharedPreferences; import android.media.tv.TvInputManager; +import android.support.annotation.IntDef; import com.android.tv.common.SharedPreferencesUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -37,13 +40,33 @@ public class DvrWatchedPositionManager { private final boolean DEBUG = false; private SharedPreferences mWatchedPositions; - private final Context mContext; private final Map<Long, Set> mListeners = new HashMap<>(); + /** + * The minimum percentage of recorded program being watched that will be considered as being + * completely watched. + */ + public static final float DVR_WATCHED_THRESHOLD_RATE = 0.98f; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({DVR_WATCHED_STATUS_NEW, DVR_WATCHED_STATUS_WATCHING, DVR_WATCHED_STATUS_WATCHED}) + public @interface DvrWatchedStatus {} + /** + * The status indicates the recorded program has not been watched at all. + */ + public static final int DVR_WATCHED_STATUS_NEW = 0; + /** + * The status indicates the recorded program is being watched. + */ + public static final int DVR_WATCHED_STATUS_WATCHING = 1; + /** + * The status indicates the recorded program was completely watched. + */ + public static final int DVR_WATCHED_STATUS_WATCHED = 2; + public DvrWatchedPositionManager(Context context) { - mContext = context.getApplicationContext(); - mWatchedPositions = mContext.getSharedPreferences(SharedPreferencesUtils - .SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); + mWatchedPositions = context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); } /** @@ -62,6 +85,18 @@ public class DvrWatchedPositionManager { TvInputManager.TIME_SHIFT_INVALID_TIME); } + @DvrWatchedStatus public int getWatchedStatus(RecordedProgram recordedProgram) { + long watchedPosition = getWatchedPosition(recordedProgram.getId()); + if (watchedPosition == TvInputManager.TIME_SHIFT_INVALID_TIME) { + return DVR_WATCHED_STATUS_NEW; + } else if (watchedPosition > recordedProgram + .getDurationMillis() * DVR_WATCHED_THRESHOLD_RATE) { + return DVR_WATCHED_STATUS_WATCHED; + } else { + return DVR_WATCHED_STATUS_WATCHING; + } + } + /** * Adds {@link WatchedPositionChangedListener}. */ diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java new file mode 100644 index 00000000..15ca2700 --- /dev/null +++ b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java @@ -0,0 +1,382 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +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.util.AsyncDbTask.AsyncProgramQueryTask; +import com.android.tv.util.AsyncDbTask.CursorFilter; +import com.android.tv.util.PermissionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. + */ +@TargetApi(Build.VERSION_CODES.N) +abstract public class EpisodicProgramLoadTask { + private static final String TAG = "EpisodicProgramLoadTask"; + + private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); + private static final int START_TIME_INDEX = + Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS); + private static final int RECORDING_PROHIBITED_INDEX = + Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); + + private static final String PARAM_START_TIME = "start_time"; + private static final String PARAM_END_TIME = "end_time"; + + private static final String PROGRAM_PREDICATE = + Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = + Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; + private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; + + private final Context mContext; + private final DvrDataManager mDataManager; + private boolean mQueryAllChannels; + private boolean mLoadCurrentProgram; + private boolean mLoadScheduledEpisode; + private boolean mLoadDisallowedProgram; + // If true, match programs with OPTION_CHANNEL_ALL. + private boolean mIgnoreChannelOption; + private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>(); + private AsyncProgramQueryTask mProgramQueryTask; + + /** + * + * Constructor used to load programs for one series recording with the given channel option. + */ + public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { + this(context, Collections.singletonList(seriesRecording)); + } + + /** + * Constructor used to load programs for multiple series recordings. The channel option is + * {@link SeriesRecording#OPTION_CHANNEL_ALL}. + */ + public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) { + mContext = context.getApplicationContext(); + mDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mSeriesRecordings.addAll(seriesRecordings); + } + + /** + * Returns the series recordings. + */ + public List<SeriesRecording> getSeriesRecordings() { + return mSeriesRecordings; + } + + /** + * Returns the program query task. It is {@code null} until it is executed. + */ + @Nullable + public AsyncProgramQueryTask getTask() { + return mProgramQueryTask; + } + + /** + * Enables loading current programs. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadCurrentProgram = loadCurrentProgram; + return this; + } + + /** + * Enables already schedules episodes. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadScheduledEpisode = loadScheduledEpisode; + return this; + } + + /** + * Enables loading disallowed programs whose schedules were removed manually by the user. + * The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadDisallowedProgram = loadDisallowedProgram; + return this; + } + + /** + * Gives the option whether to ignore the channel option when matching programs. + * If {@code ignoreChannelOption} is {@code true}, the program will be matched with + * {@link SeriesRecording#OPTION_CHANNEL_ALL} option. + */ + public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mIgnoreChannelOption = ignoreChannelOption; + return this; + } + + /** + * Executes the task. + * + * @see com.android.tv.util.AsyncDbTask#executeOnDbThread + */ + public void execute() { + if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't execute task: the task is already running.")) { + mQueryAllChannels = mSeriesRecordings.size() > 1 + || mSeriesRecordings.get(0).getChannelOption() + == SeriesRecording.OPTION_CHANNEL_ALL + || mIgnoreChannelOption; + mProgramQueryTask = createTask(); + mProgramQueryTask.executeOnDbThread(); + } + } + + /** + * Cancels the task. + * + * @see android.os.AsyncTask#cancel + */ + public void cancel(boolean mayInterruptIfRunning) { + if (mProgramQueryTask != null) { + mProgramQueryTask.cancel(mayInterruptIfRunning); + } + } + + /** + * Runs on the UI thread after the program loading finishes successfully. + */ + protected void onPostExecute(List<Program> programs) { + } + + /** + * Runs on the UI thread after the program loading was canceled. + */ + protected void onCancelled(List<Program> programs) { + } + + private AsyncProgramQueryTask createTask() { + SqlParams sqlParams = createSqlParams(); + return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, + sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) { + @Override + protected void onPostExecute(List<Program> programs) { + EpisodicProgramLoadTask.this.onPostExecute(programs); + } + + @Override + protected void onCancelled(List<Program> programs) { + EpisodicProgramLoadTask.this.onCancelled(programs); + } + }; + } + + private SqlParams createSqlParams() { + SqlParams sqlParams = new SqlParams(); + if (PermissionUtils.hasAccessAllEpg(mContext)) { + sqlParams.uri = Programs.CONTENT_URI; + // Base + StringBuilder selection = new StringBuilder(mLoadCurrentProgram + ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE); + List<String> args = new ArrayList<>(); + args.add(Long.toString(System.currentTimeMillis())); + // Channel option + if (!mQueryAllChannels) { + selection.append(" AND ").append(CHANNEL_ID_PREDICATE); + args.add(Long.toString(mSeriesRecordings.get(0).getChannelId())); + } + // Title + if (mSeriesRecordings.size() == 1) { + selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE); + args.add(mSeriesRecordings.get(0).getTitle()); + } + sqlParams.selection = selection.toString(); + sqlParams.selectionArgs = args.toArray(new String[args.size()]); + sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings); + } else { + // The query includes the current program. Will be filtered if needed. + if (mQueryAllChannels) { + sqlParams.uri = Programs.CONTENT_URI.buildUpon() + .appendQueryParameter(PARAM_START_TIME, + String.valueOf(System.currentTimeMillis())) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) + .build(); + } else { + sqlParams.uri = TvContract.buildProgramsUriForChannel( + mSeriesRecordings.get(0).getChannelId(), + System.currentTimeMillis(), Long.MAX_VALUE); + } + sqlParams.selection = null; + sqlParams.selectionArgs = null; + sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings); + } + 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<>(); + + SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { + if (!mLoadDisallowedProgram) { + mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); + } + if (!mLoadScheduledEpisode) { + Set<Long> seriesRecordingIds = new HashSet<>(); + for (SeriesRecording r : seriesRecordings) { + seriesRecordingIds.add(r.getId()); + } + for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { + if (seriesRecordingIds.contains(r.getSeriesRecordingId()) + && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + mScheduledEpisodes.add(new ScheduledEpisode(r)); + } + } + } + } + + @Override + @WorkerThread + public boolean filter(Cursor c) { + if (!mLoadDisallowedProgram + && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { + return false; + } + Program program = Program.fromCursor(c); + for (SeriesRecording seriesRecording : mSeriesRecordings) { + boolean programMatches; + if (mIgnoreChannelOption) { + programMatches = seriesRecording.matchProgram(program, + SeriesRecording.OPTION_CHANNEL_ALL); + } else { + programMatches = seriesRecording.matchProgram(program); + } + if (programMatches) { + return mLoadScheduledEpisode + || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); + } + } + return false; + } + } + + private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { + SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings) { + super(seriesRecordings); + } + + @Override + public boolean filter(Cursor c) { + return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) + && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); + } + } + + private static class SqlParams { + public Uri uri; + public String selection; + 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/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java index 23eacb73..53c89ebc 100644 --- a/src/com/android/tv/dvr/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/InputTaskScheduler.java @@ -21,7 +21,6 @@ import android.media.tv.TvInputInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; @@ -32,9 +31,11 @@ import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.util.Clock; +import com.android.tv.util.CompositeComparator; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -42,7 +43,6 @@ import java.util.Map; /** * The scheduler for a TV input. */ -@MainThread public class InputTaskScheduler { private static final String TAG = "InputTaskScheduler"; private static final boolean DEBUG = false; @@ -51,6 +51,24 @@ public class InputTaskScheduler { private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2; private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3; private static final int MSG_BUILD_SCHEDULE = 4; + private static final int MSG_STOP_SCHEDULE = 5; + + private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f; + + // The candidate comparator should be the consistent with + // DvrScheduleManager#CANDIDATE_COMPARATOR. + private static final Comparator<RecordingTask> CANDIDATE_COMPARATOR = + new CompositeComparator<>( + RecordingTask.PRIORITY_COMPARATOR, + RecordingTask.END_TIME_COMPARATOR, + RecordingTask.ID_COMPARATOR); + + /** + * Returns the comparator which the schedules are sorted with when executed. + */ + public static Comparator<ScheduledRecording> getRecordingOrderComparator() { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; + } /** * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. @@ -217,6 +235,23 @@ public class InputTaskScheduler { } } + /** + * Stops the input task scheduler. + */ + public void stop() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); + } + + private void handleStopSchedule() { + mWaitingSchedules.clear(); + int size = mPendingRecordings.size(); + for (int i = 0; i < size; ++i) { + RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; + task.cleanUp(); + } + } + @VisibleForTesting void handleBuildSchedule() { if (mWaitingSchedules.isEmpty()) { @@ -227,7 +262,8 @@ public class InputTaskScheduler { for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator(); iter.hasNext(); ) { ScheduledRecording schedule = iter.next(); - if (schedule.getEndTimeMs() <= currentTimeMs) { + if (schedule.getEndTimeMs() - currentTimeMs + <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { fail(schedule); iter.remove(); } @@ -244,7 +280,13 @@ public class InputTaskScheduler { schedulesToStart.add(schedule); } } - Collections.sort(schedulesToStart, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + // The schedules will be executed with the following order. + // 1. The schedule which starts early. It can be replaced later when the schedule with the + // higher priority needs to start. + // 2. The schedule with the higher priority. It can be replaced later when the schedule with + // the higher priority needs to start. + // 3. The schedule which was created recently. + Collections.sort(schedulesToStart, getRecordingOrderComparator()); int tunerCount; synchronized (mInputLock) { tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; @@ -266,11 +308,6 @@ public class InputTaskScheduler { task.stop(); // Just return. The schedules will be rebuilt after the task is stopped. return; - } else { - // TODO: Do not fail immediately. Start the recording later when available. - // There are no replaceable task. Remove it. - fail(schedule); - mWaitingSchedules.remove(schedule.getId()); } } } @@ -280,12 +317,20 @@ public class InputTaskScheduler { // Set next scheduling. long earliest = Long.MAX_VALUE; for (ScheduledRecording schedule : mWaitingSchedules.values()) { - if (earliest > schedule.getStartTimeMs()) { - earliest = schedule.getStartTimeMs(); + // The conflicting schedules will be removed if they end before conflicting resolved. + if (schedulesToStart.contains(schedule)) { + if (earliest > schedule.getEndTimeMs()) { + earliest = schedule.getEndTimeMs(); + } + } else { + if (earliest > schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { + earliest = schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; + } } } - mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - currentTimeMs); + mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); } private RecordingTask createRecordingTask(ScheduledRecording schedule) { @@ -309,13 +354,18 @@ public class InputTaskScheduler { } private RecordingTask getReplacableTask(ScheduledRecording schedule) { + // Returns the recording with the following priority. + // 1. The recording with the lowest priority is returned. + // 2. If the priorities are the same, the recording which finishes early is returned. + // 3. If 1) and 2) are the same, the early created schedule is returned. int size = mPendingRecordings.size(); RecordingTask candidate = null; for (int i = 0; i < size; ++i) { RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; - if (schedule.getPriority() > task.getPriority() - && (candidate == null || candidate.getPriority() > task.getPriority())) { - candidate = task; + if (schedule.getPriority() > task.getPriority()) { + if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { + candidate = task; + } } } return candidate; @@ -372,6 +422,9 @@ public class InputTaskScheduler { case MSG_BUILD_SCHEDULE: handleBuildSchedule(); break; + case MSG_STOP_SCHEDULE: + handleStopSchedule(); + break; } } } diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java index 085402a4..dd744f80 100644 --- a/src/com/android/tv/dvr/RecordedProgram.java +++ b/src/com/android/tv/dvr/RecordedProgram.java @@ -18,21 +18,25 @@ package com.android.tv.dvr; import static android.media.tv.TvContract.RecordedPrograms; +import android.annotation.TargetApi; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; +import android.os.Build; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.common.R; import com.android.tv.data.BaseProgram; +import com.android.tv.data.GenreItems; import com.android.tv.data.InternalDataUtils; import com.android.tv.util.Utils; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -40,6 +44,7 @@ import java.util.concurrent.TimeUnit; /** * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */ +@TargetApi(Build.VERSION_CODES.N) public class RecordedProgram extends BaseProgram { public static final int ID_NOT_SET = -1; @@ -553,6 +558,21 @@ public class RecordedProgram extends BaseProgram { return mCanonicalGenres; } + /** + * Returns array of canonical genre ID's for this recorded program. + */ + @Override + public int[] getCanonicalGenreIds() { + if (mCanonicalGenres == null) { + return null; + } + int[] genreIds = new int[mCanonicalGenres.length]; + for (int i = 0; i < mCanonicalGenres.length; i++) { + genreIds[i] = GenreItems.getId(mCanonicalGenres[i]); + } + return genreIds; + } + @Override public long getChannelId() { return mChannelId; @@ -622,6 +642,21 @@ public class RecordedProgram extends BaseProgram { } } + @Nullable + public String getEpisodeDisplayNumber(Context context) { + if (!TextUtils.isEmpty(mEpisodeNumber)) { + if (TextUtils.equals(mSeasonNumber, "0")) { + // Do not show "S0: ". + return String.format(context.getResources().getString( + R.string.display_episode_number_format_no_season_number), mEpisodeNumber); + } else { + return String.format(context.getResources().getString( + R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber); + } + } + return null; + } + public long getExpireTimeUtcMillis() { return mExpireTimeUtcMillis; } @@ -678,6 +713,7 @@ public class RecordedProgram extends BaseProgram { return mSearchable; } + @Override public String getSeriesId() { return mSeriesId; } @@ -822,4 +858,11 @@ public class RecordedProgram extends BaseProgram { private static String safeEncode(@Nullable String[] genres) { return genres == null ? null : TvContract.Programs.Genres.encode(genres); } + + /** + * Returns an array containing all of the elements in the list. + */ + public static RecordedProgram[] toArray(Collection<RecordedProgram> recordedPrograms) { + return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]); + } } diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java index 2373f15c..c3d236b0 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -41,6 +41,7 @@ import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; +import java.util.Comparator; import java.util.concurrent.TimeUnit; /** @@ -57,6 +58,39 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private static final String TAG = "RecordingTask"; private static final boolean DEBUG = false; + /** + * Compares the end time in ascending order. + */ + public static final Comparator<RecordingTask> END_TIME_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); + } + }; + + /** + * Compares ID in ascending order. + */ + public static final Comparator<RecordingTask> ID_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); + } + }; + + /** + * Compares the priority in ascending order. + */ + public static final Comparator<RecordingTask> PRIORITY_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getPriority(), rhs.getPriority()); + } + }; + @VisibleForTesting static final int MSG_INITIALIZE = 1; @VisibleForTesting @@ -169,6 +203,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); + if (mRecordingSession != null) { + failAndQuit(); + } + } + + @Override public void onTuned(Uri channelUri) { if (DEBUG) Log.d(TAG, "onTuned"); if (mRecordingSession == null) { @@ -252,7 +294,8 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback String inputId = mChannel.getInputId(); mRecordingSession = mSessionManager.createRecordingSession(inputId, - "recordingTask-" + mScheduledRecording.getId(), this, mHandler); + "recordingTask-" + mScheduledRecording.getId(), this, + mHandler, mScheduledRecording.getEndTimeMs()); mState = State.SESSION_ACQUIRED; mDvrManager.addListener(this, mHandler); mRecordingSession.tune(inputId, mChannel.getUri()); @@ -302,11 +345,15 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private void handleUpdateSchedule(ScheduledRecording schedule) { mScheduledRecording = schedule; // Check end time only. The start time is checked in InputTaskScheduler. - if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs() - && mState == State.RECORDING_STARTED) { - mHandler.removeMessages(MSG_STOP_RECORDING); - if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { - failAndQuit(); + if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { + if (mRecordingSession != null) { + mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); + } + if (mState == State.RECORDING_STARTED) { + mHandler.removeMessages(MSG_STOP_RECORDING); + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { + failAndQuit(); + } } } } @@ -316,6 +363,10 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback return mState; } + private long getScheduleId() { + return mScheduledRecording.getId(); + } + /** * Returns the priority. */ @@ -359,7 +410,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) .build(); - mMainThreadHandler.post(new Runnable() { + runOnMainThread(new Runnable() { @Override public void run() { ScheduledRecording schedule = mDataManager.getScheduledRecording( @@ -429,6 +480,19 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback removeRecordedProgram(); } + /** + * Clean up the task. + */ + public void cleanUp() { + if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + } + release(); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + } + @Override public String toString() { return getClass().getName() + "(" + mScheduledRecording + ")"; diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java index a9673b40..2bda10ea 100644 --- a/src/com/android/tv/dvr/ScheduledRecording.java +++ b/src/com/android/tv/dvr/ScheduledRecording.java @@ -31,6 +31,7 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.dvr.provider.DvrContract.Schedules; +import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; import java.lang.annotation.Retention; @@ -56,6 +57,9 @@ public final class ScheduledRecording implements Parcelable { */ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; + /** + * Compares the start time in ascending order. + */ public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() { @Override @@ -65,7 +69,7 @@ public final class ScheduledRecording implements Parcelable { }; /** - * Compare the end time in ascending order. + * Compares the end time in ascending order. */ public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR = new Comparator<ScheduledRecording>() { @@ -76,34 +80,36 @@ public final class ScheduledRecording implements Parcelable { }; /** - * Compare priority in descending order. + * Compares ID in ascending order. The schedule with the larger ID was created later. */ - public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR + public static final Comparator<ScheduledRecording> ID_COMPARATOR = new Comparator<ScheduledRecording>() { @Override public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - int value = Long.compare(rhs.mPriority, lhs.mPriority); - if (value == 0) { - // New recording has the higher priority. - value = Long.compare(rhs.mId, lhs.mId); - } - return value; + return Long.compare(lhs.mId, rhs.mId); } }; - public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR + /** + * Compares the priority in ascending order. + */ + public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() { @Override public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - int value = START_TIME_COMPARATOR.compare(lhs, rhs); - if (value == 0) { - value = PRIORITY_COMPARATOR.compare(lhs, rhs); - } - return value; + return Long.compare(lhs.mPriority, rhs.mPriority); } }; /** + * Compares start time in ascending order and then priority in descending order and then ID in + * descending order. + */ + public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(), + ID_COMPARATOR.reversed()); + + /** * Builds scheduled recordings from programs. */ public static Builder builder(String inputId, Program p) { @@ -285,6 +291,7 @@ public final class ScheduledRecording implements Parcelable { .setChannelId(orig.mChannelId) .setEndTimeMs(orig.mEndTimeMs) .setSeriesRecordingId(orig.mSeriesRecordingId) + .setPriority(orig.mPriority) .setProgramId(orig.mProgramId) .setProgramTitle(orig.mProgramTitle) .setStartTimeMs(orig.mStartTimeMs) @@ -766,6 +773,13 @@ public final class ScheduledRecording implements Parcelable { return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); } + /** + * Checks if the {@code schedule} overlaps with this schedule. + */ + public boolean isOverLapping(ScheduledRecording schedule) { + return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs(); + } + @Override public String toString() { return "ScheduledRecording[" + mId @@ -775,8 +789,8 @@ public final class ScheduledRecording implements Parcelable { + ",programId=" + mProgramId + ",programTitle=" + mProgramTitle + ",type=" + mType - + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) - + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")" + + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")" + ",seasonNumber=" + mSeasonNumber + ",episodeNumber=" + mEpisodeNumber + ",episodeTitle=" + mEpisodeTitle diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java index 25904ee4..ce78e1be 100644 --- a/src/com/android/tv/dvr/Scheduler.java +++ b/src/com/android/tv/dvr/Scheduler.java @@ -123,6 +123,9 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList * Stops the scheduler. */ public void stop() { + for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { + inputTaskScheduler.stop(); + } mInputManager.removeCallback(this); mDataManager.removeScheduledRecordingListener(this); } @@ -173,13 +176,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } boolean needToUpdateAlarm = false; for (ScheduledRecording schedule : schedules) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { - Log.e(TAG, "Can't find input for " + schedule); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); - continue; - } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); if (scheduler != null) { scheduler.removeSchedule(schedule); needToUpdateAlarm = true; @@ -198,12 +195,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } // Update the recordings. for (ScheduledRecording schedule : schedules) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { - Log.e(TAG, "Can't find input for " + schedule); - continue; - } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); if (scheduler != null) { scheduler.updateSchedule(schedule); } @@ -228,7 +220,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } private void scheduleRecordingSoon(ScheduledRecording schedule) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); if (input == null) { Log.e(TAG, "Can't find input for " + schedule); mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); @@ -260,7 +252,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); // This will cancel the previous alarm. - mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); + mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); } else { if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); } diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java index fc68eaf7..f0690f5f 100644 --- a/src/com/android/tv/dvr/SeriesRecording.java +++ b/src/com/android/tv/dvr/SeriesRecording.java @@ -21,10 +21,10 @@ import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; @@ -69,8 +69,8 @@ public class SeriesRecording implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, - value = {STATE_SERIES_NORMAL, STATE_SERIES_CANCELED}) - private @interface SeriesState {} + value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) + public @interface SeriesState {} /** * The state indicates that the series recording is a normal one. @@ -78,9 +78,9 @@ public class SeriesRecording implements Parcelable { public static final int STATE_SERIES_NORMAL = 0; /** - * The state indicates that the series recording is canceled. + * The state indicates that the series recording is stopped. */ - public static final int STATE_SERIES_CANCELED = 1; + public static final int STATE_SERIES_STOPPED = 1; /** * Compare priority in descending order. @@ -110,9 +110,9 @@ public class SeriesRecording implements Parcelable { }; /** - * Creates a new Builder with the values set from the series information of {@link Program}. + * Creates a new Builder with the values set from the series information of {@link BaseProgram}. */ - public static Builder builder(String inputId, Program p) { + public static Builder builder(String inputId, BaseProgram p) { return new Builder() .setInputId(inputId) .setSeriesId(p.getSeriesId()) @@ -190,7 +190,7 @@ public class SeriesRecording implements Parcelable { .setCanonicalGenreIds(c.getString(++index)) .setPosterUri(c.getString(++index)) .setPhotoUri(c.getString(++index)) - .setState(seriesRecordingCanceled(c.getString(++index))) + .setState(seriesRecordingState(c.getString(++index))) .build(); } @@ -220,7 +220,7 @@ public class SeriesRecording implements Parcelable { Utils.getCanonicalGenre(r.getCanonicalGenreIds())); values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri()); values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri()); - values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingCanceled(r.getState())); + values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState())); return values; } @@ -244,22 +244,22 @@ public class SeriesRecording implements Parcelable { return OPTION_CHANNEL_ONE; } - private static String seriesRecordingCanceled(@SeriesState int state) { + private static String seriesRecordingState(@SeriesState int state) { switch (state) { case STATE_SERIES_NORMAL: return SeriesRecordings.STATE_SERIES_NORMAL; - case STATE_SERIES_CANCELED: - return SeriesRecordings.STATE_SERIES_CANCELED; + case STATE_SERIES_STOPPED: + return SeriesRecordings.STATE_SERIES_STOPPED; } return SeriesRecordings.STATE_SERIES_NORMAL; } - @SeriesState private static int seriesRecordingCanceled(String state) { + @SeriesState private static int seriesRecordingState(String state) { switch (state) { case SeriesRecordings.STATE_SERIES_NORMAL: return STATE_SERIES_NORMAL; - case SeriesRecordings.STATE_SERIES_CANCELED: - return STATE_SERIES_CANCELED; + case SeriesRecordings.STATE_SERIES_STOPPED: + return STATE_SERIES_STOPPED; } return STATE_SERIES_NORMAL; } @@ -331,6 +331,7 @@ public class SeriesRecording implements Parcelable { mInputId = inputId; return this; } + /** * @see #getChannelId() */ @@ -478,7 +479,7 @@ public class SeriesRecording implements Parcelable { } /** - * The channelId to match. + * The channelId to match. The channel ID might not be valid when the channel option is "ALL". */ public long getChannelId() { return mChannelId; @@ -534,7 +535,6 @@ public class SeriesRecording implements Parcelable { * * <p>SeriesId is an opaque but stable string. */ - @NonNull public String getSeriesId() { return mSeriesId; } @@ -590,6 +590,13 @@ public class SeriesRecording implements Parcelable { return mState; } + /** + * Checks whether the series recording is stopped or not. + */ + public boolean isStopped() { + return mState == STATE_SERIES_STOPPED; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -695,7 +702,7 @@ public class SeriesRecording implements Parcelable { * episode constraints. */ public boolean matchProgram(Program program) { - return matchProgram(program, true); + return matchProgram(program, mChannelOption); } /** @@ -703,13 +710,12 @@ public class SeriesRecording implements Parcelable { * episode constraints. It checks the channel option only if {@code checkChannelOption} is * {@code true}. */ - public boolean matchProgram(Program program, boolean checkChannelOption) { + public boolean matchProgram(Program program, @ChannelOption int channelOption) { String seriesId = program.getSeriesId(); long channelId = program.getChannelId(); String seasonNumber = program.getSeasonNumber(); String episodeNumber = program.getEpisodeNumber(); - if (!mSeriesId.equals(seriesId) || (checkChannelOption - && mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE && mChannelId != channelId)) { return false; } diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java index 9e9b3add..5ed12ce8 100644 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/SeriesRecordingScheduler.java @@ -20,19 +20,14 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvContract.Programs; -import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.MainThread; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import android.util.LongSparseArray; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; @@ -43,10 +38,8 @@ import com.android.tv.data.Program; import com.android.tv.data.epg.EpgFetcher; 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.experiments.Experiments; -import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; -import com.android.tv.util.AsyncDbTask.CursorFilter; -import com.android.tv.util.PermissionUtils; import java.util.ArrayList; import java.util.Arrays; @@ -59,7 +52,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.Set; /** @@ -70,20 +63,7 @@ import java.util.Set; @TargetApi(Build.VERSION_CODES.N) public class SeriesRecordingScheduler { private static final String TAG = "SeriesRecordingSchd"; - - private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); - private static final int RECORDING_PROHIBITED_INDEX = - Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); - - private static final String PARAM_START_TIME = "start_time"; - private static final String PARAM_END_TIME = "end_time"; - - private static final String PROGRAM_SELECTION = - Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND (" + - Programs.COLUMN_SEASON_DISPLAY_NUMBER + " IS NOT NULL OR " + - Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " IS NOT NULL) AND " + - Programs.COLUMN_RECORDING_PROHIBITED + "=0"; - private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; + private static final boolean DEBUG = false; private static final String KEY_FETCHED_SERIES_IDS = "SeriesRecordingScheduler.fetched_series_ids"; @@ -109,6 +89,11 @@ public class SeriesRecordingScheduler { 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 @@ -124,7 +109,7 @@ public class SeriesRecordingScheduler { for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.subtract(task.mSeriesRecordings, seriesRecordings, + if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR).isEmpty()) { task.cancel(true); iter.remove(); @@ -134,7 +119,21 @@ public class SeriesRecordingScheduler { @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - updateSchedules(Arrays.asList(seriesRecordings)); + List<SeriesRecording> stopped = new ArrayList<>(); + List<SeriesRecording> normal = new ArrayList<>(); + for (SeriesRecording r : seriesRecordings) { + if (r.isStopped()) { + stopped.add(r); + } else { + normal.add(r); + } + } + if (!stopped.isEmpty()) { + onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); + } + if (!normal.isEmpty()) { + updateSchedules(normal); + } } }; @@ -174,8 +173,6 @@ public class SeriesRecordingScheduler { Set<Long> seriesRecordingIds = new HashSet<>(); for (ScheduledRecording r : schedules) { if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { - SoftPreconditions.checkState(r.getState() - != ScheduledRecording.STATE_RECORDING_FINISHED); seriesRecordingIds.add(r.getSeriesRecordingId()); } } @@ -214,6 +211,7 @@ public class SeriesRecordingScheduler { if (mStarted) { return; } + if (DEBUG) Log.d(TAG, "start"); mStarted = true; mDataManager.addSeriesRecordingListener(mSeriesRecordingListener); mDataManager.addScheduledRecordingListener(mScheduledRecordingListener); @@ -226,13 +224,16 @@ public class SeriesRecordingScheduler { if (!mStarted) { return; } + if (DEBUG) Log.d(TAG, "stop"); mStarted = false; for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { task.cancel(true); } + mFetchSeriesInfoTasks.clear(); for (SeriesRecordingUpdateTask task : mScheduleTasks) { task.cancel(true); } + mScheduleTasks.clear(); mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); } @@ -254,26 +255,81 @@ public class SeriesRecordingScheduler { } /** - * Creates/Updates the schedules for all the series recordings. + * Pauses the updates of the series recordings. */ - @MainThread - public void updateSchedules() { + public void pauseUpdate() { + if (DEBUG) Log.d(TAG, "Schedule paused"); + if (mPaused) { + return; + } + mPaused = true; if (!mStarted) { return; } - updateSchedules(mDataManager.getSeriesRecordings()); + for (SeriesRecordingUpdateTask task : mScheduleTasks) { + for (SeriesRecording r : task.getSeriesRecordings()) { + mPendingSeriesRecordings.add(r.getId()); + } + task.cancel(true); + } + } + + /** + * Resumes the updates of the series recordings. + */ + public void resumeUpdate() { + if (DEBUG) Log.d(TAG, "Schedule resumed"); + if (!mPaused) { + return; + } + mPaused = false; + if (!mStarted) { + return; + } + if (!mPendingSeriesRecordings.isEmpty()) { + List<SeriesRecording> seriesRecordings = new ArrayList<>(); + for (long seriesRecordingId : mPendingSeriesRecordings) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(seriesRecordingId); + if (seriesRecording != null) { + seriesRecordings.add(seriesRecording); + } + } + if (!seriesRecordings.isEmpty()) { + updateSchedules(seriesRecordings); + } + } } - private void updateSchedules(Collection<SeriesRecording> seriesRecordings) { + /** + * Update schedules for the given series recordings. If it's paused, the update will be done + * after it's resumed. + */ + public void updateSchedules(Collection<SeriesRecording> seriesRecordings) { + if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings); + if (!mStarted) { + if (DEBUG) Log.d(TAG, "Not started yet."); + return; + } + if (mPaused) { + for (SeriesRecording r : seriesRecordings) { + mPendingSeriesRecordings.add(r.getId()); + } + if (DEBUG) { + Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size=" + + mPendingSeriesRecordings.size()); + } + return; + } Set<SeriesRecording> previousSeriesRecordings = new HashSet<>(); for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.containsAny(task.mSeriesRecordings, seriesRecordings, + if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR)) { // The task is affected by the seriesRecordings task.cancel(true); - previousSeriesRecordings.addAll(task.mSeriesRecordings); + previousSeriesRecordings.addAll(task.getSeriesRecordings()); iter.remove(); } } @@ -281,38 +337,44 @@ public class SeriesRecordingScheduler { previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator(); iter.hasNext(); ) { - if (mDataManager.getSeriesRecording(iter.next().getId()) == null) { - // Series recording has been removed. + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); + if (seriesRecording == null || seriesRecording.isStopped()) { + // Series recording has been removed or stopped. iter.remove(); } } if (seriesRecordingsToUpdate.isEmpty()) { return; } - List<SeriesRecordingUpdateTask> tasksToRun = new ArrayList<>(); if (needToReadAllChannels(seriesRecordingsToUpdate)) { - SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask(seriesRecordingsToUpdate, - createSqlParams(seriesRecordingsToUpdate, null)); - tasksToRun.add(task); + SeriesRecordingUpdateTask task = + new SeriesRecordingUpdateTask(seriesRecordingsToUpdate); mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); } else { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask( - Collections.singletonList(seriesRecording), - createSqlParams(Collections.singletonList(seriesRecording), null)); - tasksToRun.add(task); + Collections.singletonList(seriesRecording)); mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); } } - if (mDataManager.isDvrScheduleLoadFinished()) { - runTasks(tasksToRun); - } } - private void runTasks(List<SeriesRecordingUpdateTask> tasks) { - for (SeriesRecordingUpdateTask task : tasks) { - task.executeOnDbThread(); - } + /** + * 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) { @@ -325,94 +387,6 @@ public class SeriesRecordingScheduler { } /** - * Queries the programs which are related to the series. - * <p> - * This is called from the UI when the series recording is created. - */ - public void queryPrograms(SeriesRecording series, ProgramLoadCallback callback) { - SoftPreconditions.checkState(mDataManager.isInitialized()); - Set<ScheduledEpisode> scheduledEpisodes = new HashSet<>(); - for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) { - if (series.getSeriesId().equals(recordedProgram.getSeriesId())) { - scheduledEpisodes.add(new ScheduledEpisode(series.getId(), - recordedProgram.getSeasonNumber(), recordedProgram.getEpisodeNumber())); - } - } - SqlParams sqlParams = createSqlParams(Collections.singletonList(series), scheduledEpisodes); - new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection, - sqlParams.selectionArgs, null, sqlParams.filter) { - @Override - protected void onPostExecute(List<Program> programs) { - SoftPreconditions.checkNotNull(programs); - if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " + series); - callback.onProgramLoadFinished(Collections.emptyList()); - } else { - Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode( - Collections.singletonList(series), programs); - callback.onProgramLoadFinished(seriesProgramMap.get(series.getId())); - } - } - }.executeOnDbThread(); - // To shorten the response time from UI, cancel and restart the background job. - restartTasks(); - } - - private void restartTasks() { - Set<SeriesRecording> seriesRecordings = new HashSet<>(); - for (SeriesRecordingUpdateTask task : mScheduleTasks) { - seriesRecordings.addAll(task.mSeriesRecordings); - task.cancel(true); - } - mScheduleTasks.clear(); - updateSchedules(seriesRecordings); - } - - private SqlParams createSqlParams(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - SqlParams sqlParams = new SqlParams(); - if (PermissionUtils.hasAccessAllEpg(mContext)) { - sqlParams.uri = Programs.CONTENT_URI; - if (needToReadAllChannels(seriesRecordings)) { - sqlParams.selection = PROGRAM_SELECTION; - sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis())}; - } else { - SoftPreconditions.checkArgument(seriesRecordings.size() == 1); - sqlParams.selection = PROGRAM_SELECTION + " AND " + CHANNEL_ID_PREDICATE; - sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis()), - Long.toString(seriesRecordings.get(0).getChannelId())}; - } - sqlParams.filter = new SeriesRecordingCursorFilter(seriesRecordings, scheduledEpisodes); - } else { - if (needToReadAllChannels(seriesRecordings)) { - sqlParams.uri = Programs.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_START_TIME, - String.valueOf(System.currentTimeMillis())) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) - .build(); - } else { - SoftPreconditions.checkArgument(seriesRecordings.size() == 1); - sqlParams.uri = TvContract.buildProgramsUriForChannel( - seriesRecordings.get(0).getChannelId(), - System.currentTimeMillis(), Long.MAX_VALUE); - } - sqlParams.selection = null; - sqlParams.selectionArgs = null; - sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(seriesRecordings, - scheduledEpisodes); - } - 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); - } - - /** * Pick one program per an episode. * * <p>Note that the programs which has been already scheduled have the highest priority, and all @@ -421,7 +395,7 @@ public class SeriesRecordingScheduler { * <p>If there are no existing schedules for an episode, one program which starts earlier is * picked. */ - private Map<Long, List<Program>> pickOneProgramPerEpisode( + private LongSparseArray<List<Program>> pickOneProgramPerEpisode( List<SeriesRecording> seriesRecordings, List<Program> programs) { return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); } @@ -430,10 +404,11 @@ public class SeriesRecordingScheduler { * @see #pickOneProgramPerEpisode(List, List) */ @VisibleForTesting - static Map<Long, List<Program>> pickOneProgramPerEpisode(DvrDataManager dataManager, - List<SeriesRecording> seriesRecordings, List<Program> programs) { + static LongSparseArray<List<Program>> pickOneProgramPerEpisode( + DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, + List<Program> programs) { // Initialize. - Map<Long, List<Program>> result = new HashMap<>(); + LongSparseArray<List<Program>> result = new LongSparseArray<>(); Map<String, Long> seriesRecordingIds = new HashMap<>(); for (SeriesRecording seriesRecording : seriesRecordings) { result.put(seriesRecording.getId(), new ArrayList<>()); @@ -508,27 +483,27 @@ public class SeriesRecordingScheduler { * This works only for the existing series recordings. Do not use this task for the * "adding series recording" UI. */ - private class SeriesRecordingUpdateTask extends AsyncProgramQueryTask { - private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>(); - - SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings, SqlParams sqlParams) { - super(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection, - sqlParams.selectionArgs, null, sqlParams.filter); - mSeriesRecordings.addAll(seriesRecordings); + private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { + SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) { + super(mContext, seriesRecordings); } @Override protected void onPostExecute(List<Program> programs) { + if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); mScheduleTasks.remove(this); if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " + mSeriesRecordings); + Log.e(TAG, "Creating schedules for series recording failed: " + + getSeriesRecordings()); return; } - Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode( - mSeriesRecordings, programs); - for (SeriesRecording seriesRecording : mSeriesRecordings) { + LongSparseArray<List<Program>> seriesProgramMap = pickOneProgramPerEpisode( + getSeriesRecordings(), programs); + for (SeriesRecording seriesRecording : getSeriesRecordings()) { // Check the series recording is still valid. - if (mDataManager.getSeriesRecording(seriesRecording.getId()) == null) { + SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( + seriesRecording.getId()); + if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { continue; } List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); @@ -537,122 +512,25 @@ public class SeriesRecordingScheduler { mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); } } - } - - @Override - protected void onCancelled(List<Program> programs) { - mScheduleTasks.remove(this); - } - } - - /** - * 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 List<SeriesRecording> mSeriesRecording = new ArrayList<>(); - private final Set<Long> mDisallowedProgramIds = new HashSet<>(); - private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); - - SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - mSeriesRecording.addAll(seriesRecordings); - mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); - Set<Long> seriesRecordingIds = new HashSet<>(); - for (SeriesRecording r : seriesRecordings) { - seriesRecordingIds.add(r.getId()); - } - if (scheduledEpisodes != null) { - mScheduledEpisodes.addAll(scheduledEpisodes); - } - for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { - if (seriesRecordingIds.contains(r.getSeriesRecordingId()) - && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED - && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mScheduledEpisodes.add(new ScheduledEpisode(r)); + if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { + for (OnSeriesRecordingUpdatedListener listener + : mOnSeriesRecordingUpdatedListeners) { + listener.onSeriesRecordingUpdated( + SeriesRecording.toArray(getSeriesRecordings())); } } } @Override - @WorkerThread - public boolean filter(Cursor c) { - if (mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { - return false; - } - Program program = Program.fromCursor(c); - for (SeriesRecording seriesRecording : mSeriesRecording) { - boolean programMatches = seriesRecording.matchProgram(program); - if (programMatches && !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber()))) { - return true; - } - } - return false; - } - } - - private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { - SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - super(seriesRecordings, scheduledEpisodes); - } - - @Override - public boolean filter(Cursor c) { - return c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); - } - } - - private static class SqlParams { - public Uri uri; - public String selection; - public String[] selectionArgs; - public CursorFilter filter; - } - - @VisibleForTesting - 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); + protected void onCancelled(List<Program> programs) { + mScheduleTasks.remove(this); } @Override public String toString() { - return "ScheduledEpisode{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; + return "SeriesRecordingUpdateTask:{" + + "series_recordings=" + getSeriesRecordings() + + "}"; } } @@ -663,10 +541,6 @@ public class SeriesRecordingScheduler { mSeriesRecording = seriesRecording; } - String getSeriesId() { - return mSeriesRecording.getSeriesId(); - } - @Override protected SeriesInfo doInBackground(Void... voids) { return EpgFetcher.createEpgReader(mContext) @@ -697,9 +571,9 @@ public class SeriesRecordingScheduler { } /** - * Called when the program loading is finished for the series recording. + * A listener to notify when series recording are updated. */ - public interface ProgramLoadCallback { - void onProgramLoadFinished(@NonNull List<Program> programs); + public interface OnSeriesRecordingUpdatedListener { + void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); } } diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 382f7112..bf72d912 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -44,9 +44,13 @@ interface WritableDvrDataManager extends DvrDataManager { void removeScheduledRecording(ScheduledRecording... scheduledRecordings); /** + * Removes recordings. If {@code forceRemove} is {@code true}, the schedule will be permanently + * removed instead of changing the state to DELETED. + */ + void removeScheduledRecording(boolean forceRemove, ScheduledRecording... scheduledRecordings); + + /** * Removes series recordings. - * - * <p>Note that the finished or failed schedules are not deleted. */ void removeSeriesRecording(SeriesRecording... seasonSchedules); @@ -64,4 +68,11 @@ interface WritableDvrDataManager extends DvrDataManager { * Changes the state of the recording. */ void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState); + + /** + * Remove all the records related to the input. + * <p> + * Note that this should be called after the input was removed. + */ + void forgetStorage(String inputId); } diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java index 3fe2d211..f0aca18e 100644 --- a/src/com/android/tv/dvr/provider/DvrContract.java +++ b/src/com/android/tv/dvr/provider/DvrContract.java @@ -233,9 +233,9 @@ public final class DvrContract { public static final String STATE_SERIES_NORMAL = "STATE_SERIES_NORMAL"; /** - * The state indicates that it is a canceled one. + * The state indicates that it is stopped. */ - public static final String STATE_SERIES_CANCELED = "STATE_SERIES_CANCELED"; + public static final String STATE_SERIES_STOPPED = "STATE_SERIES_STOPPED"; /** * The priority of this recording. @@ -380,7 +380,7 @@ public final class DvrContract { * The state of whether the series recording be canceled or not. * * <p>This value should be one of the followings: {@link #STATE_SERIES_NORMAL} and - * {@link #STATE_SERIES_CANCELED}. The default value is STATE_SERIES_NORMAL. + * {@link #STATE_SERIES_STOPPED}. The default value is STATE_SERIES_NORMAL. * * <p>Type: TEXT */ diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java index d6e17161..175f05bc 100644 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java @@ -16,27 +16,285 @@ package com.android.tv.dvr.ui; -import android.content.Context; -import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; +import android.app.Activity; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.support.v17.leanback.widget.Presenter; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.tv.R; +import com.android.tv.ui.ViewUtils; import com.android.tv.util.Utils; /** - * Presents a {@link DetailsContent}. + * An {@link Presenter} for rendering a detailed description of an DVR item. + * Typically this Presenter will be used in a {@link 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 AbstractDetailsDescriptionPresenter { +public class DetailsContentPresenter extends Presenter { + /** + * The ViewHolder for the {@link DetailsContentPresenter}. + */ + public static class ViewHolder extends Presenter.ViewHolder { + final TextView mTitle; + final TextView mSubtitle; + final LinearLayout mDescriptionContainer; + final TextView mBody; + final TextView mReadMoreView; + final int mTitleMargin; + final int mUnderTitleBaselineMargin; + final int mUnderSubtitleBaselineMargin; + final int mTitleLineSpacing; + final int mBodyLineSpacing; + final int mBodyMaxLines; + final int mBodyMinLines; + final FontMetricsInt mTitleFontMetricsInt; + final FontMetricsInt mSubtitleFontMetricsInt; + final FontMetricsInt mBodyFontMetricsInt; + final int mTitleMaxLines; + + private Activity mActivity; + private boolean mFullTextMode; + private int mFullTextAnimationDuration; + private boolean mIsListeningToPreDraw; + + private ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (mSubtitle.getVisibility() == View.VISIBLE + && mSubtitle.getTop() > view.getHeight() + && mTitle.getLineCount() > 1) { + mTitle.setMaxLines(mTitle.getLineCount() - 1); + return false; + } + final int bodyLines = mBody.getLineCount(); + final int maxLines = mFullTextMode ? bodyLines : + (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); + if (bodyLines > maxLines) { + mReadMoreView.setVisibility(View.VISIBLE); + mDescriptionContainer.setFocusable(true); + mDescriptionContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mFullTextMode = true; + mReadMoreView.setVisibility(View.GONE); + mDescriptionContainer.setFocusable(false); + mDescriptionContainer.setOnClickListener(null); + mBody.setMaxLines(bodyLines); + // Minus 1 from line difference to eliminate the space + // originally occupied by "READ MORE" + showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); + } + }); + } + if (mBody.getMaxLines() != maxLines) { + mBody.setMaxLines(maxLines); + return false; + } else { + removePreDrawListener(); + return true; + } + } + }; + + public ViewHolder(final View view) { + super(view); + 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); + mDescriptionContainer = + (LinearLayout) view.findViewById(R.id.dvr_details_description_container); + mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); + + FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); + final int titleAscent = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_baseline); + // Ascent is negative + mTitleMargin = titleAscent + titleFontMetricsInt.ascent; + + mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_title_baseline_margin); + mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_subtitle_baseline_margin); + + mTitleLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_line_spacing); + mBodyLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_body_line_spacing); + + mBodyMaxLines = view.getResources().getInteger( + R.integer.lb_details_description_body_max_lines); + mBodyMinLines = view.getResources().getInteger( + R.integer.lb_details_description_body_min_lines); + mTitleMaxLines = mTitle.getMaxLines(); + + mTitleFontMetricsInt = getFontMetricsInt(mTitle); + mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); + mBodyFontMetricsInt = getFontMetricsInt(mBody); + } + + void addPreDrawListener() { + if (!mIsListeningToPreDraw) { + mIsListeningToPreDraw = true; + view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + } + + void removePreDrawListener() { + if (mIsListeningToPreDraw) { + view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); + mIsListeningToPreDraw = false; + } + } + + public TextView getTitle() { + return mTitle; + } + + public TextView getSubtitle() { + return mSubtitle; + } + + public TextView getBody() { + return mBody; + } + + private FontMetricsInt getFontMetricsInt(TextView textView) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setTextSize(textView.getTextSize()); + paint.setTypeface(textView.getTypeface()); + return paint.getFontMetricsInt(); + } + + private void showFullText(int heightDiff) { + final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); + int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); + Animator expandAnimator = ViewUtils.createHeightAnimator( + detailsFrame, nowHeight, nowHeight + heightDiff); + expandAnimator.setDuration(mFullTextAnimationDuration); + Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + 0f, -(heightDiff / 2))); + shiftAnimator.setDuration(mFullTextAnimationDuration); + AnimatorSet fullTextAnimator = new AnimatorSet(); + fullTextAnimator.playTogether(expandAnimator, shiftAnimator); + fullTextAnimator.start(); + } + } + + private final Activity mActivity; + private final int mFullTextAnimationDuration; + + public DetailsContentPresenter(Activity activity) { + super(); + mActivity = activity; + mFullTextAnimationDuration = mActivity.getResources() + .getInteger(R.integer.dvr_details_full_text_animation_duration); + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.dvr_details_description, parent, false); + return new ViewHolder(v); + } + @Override - protected void onBindDescription(final ViewHolder viewHolder, Object itemData) { - DetailsContent detailsContent = (DetailsContent) itemData; - Context context = viewHolder.view.getContext(); - viewHolder.getTitle().setText(detailsContent.getTitle()); + public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + final ViewHolder vh = (ViewHolder) viewHolder; + final DetailsContent detailsContent = (DetailsContent) item; + + vh.mActivity = mActivity; + vh.mFullTextAnimationDuration = mFullTextAnimationDuration; + + boolean hasTitle = true; + if (TextUtils.isEmpty(detailsContent.getTitle())) { + vh.mTitle.setVisibility(View.GONE); + hasTitle = false; + } else { + vh.mTitle.setText(detailsContent.getTitle()); + vh.mTitle.setVisibility(View.VISIBLE); + vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() + + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); + vh.mTitle.setMaxLines(vh.mTitleMaxLines); + } + setTopMargin(vh.mTitle, vh.mTitleMargin); + + boolean hasSubtitle = true; if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { - String playTime = Utils.getDurationString(context, + vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), detailsContent.getStartTimeUtcMillis(), - detailsContent.getEndTimeUtcMillis(), false); - viewHolder.getSubtitle().setText(playTime); + detailsContent.getEndTimeUtcMillis(), false)); + vh.mSubtitle.setVisibility(View.VISIBLE); + if (hasTitle) { + setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin + + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); + } else { + setTopMargin(vh.mSubtitle, 0); + } + } else { + vh.mSubtitle.setVisibility(View.GONE); + hasSubtitle = false; } - viewHolder.getBody().setText(detailsContent.getDescription()); + + if (TextUtils.isEmpty(detailsContent.getDescription())) { + vh.mBody.setVisibility(View.GONE); + } else { + vh.mBody.setText(detailsContent.getDescription()); + vh.mBody.setVisibility(View.VISIBLE); + vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() + + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); + if (hasSubtitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else if (hasTitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else { + setTopMargin(vh.mDescriptionContainer, 0); + } + } + } + + @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; + view.setLayoutParams(lp); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java index 37f152f9..6714ecd3 100644 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java @@ -76,13 +76,17 @@ public class DetailsViewBackgroundHelper { * Sets the background color. */ public void setBackgroundColor(int color) { - mBackgroundManager.setColor(color); + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setColor(color); + } } /** * Sets the background scrim. */ public void setScrim(int color) { - mBackgroundManager.setDimLayer(new ColorDrawable(color)); + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setDimLayer(new ColorDrawable(color)); + } } } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index d7c2de88..9df228d1 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -96,7 +96,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { if (action.getId() == ACTION_RECORD_ANYWAY) { getDvrManager().addSchedule(mProgram); } else if (action.getId() == ACTION_WATCH) { - DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null); + DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null, false); } dismissDialog(); } diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java index 74d0ba0b..a6dd31d1 100644 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java @@ -26,23 +26,25 @@ 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 com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.GenreItems; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; +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.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.Arrays; @@ -64,7 +66,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private RecordedProgramAdapter mRecentAdapter; private ScheduleAdapter mScheduleAdapter; - private RecordedProgramAdapter mSeriesAdapter; + private SeriesAdapter mSeriesAdapter; private RecordedProgramAdapter[] mGenreAdapters = new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; private ListRow mRecentRow; @@ -72,7 +74,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; private List<String> mGenreLabels; private DvrDataManager mDvrDataManager; - private TvInputManagerHelper mTvInputManagerHelper; + private DvrScheduleManager mDvrScheudleManager; private ArrayObjectAdapter mRowsAdapter; private ClassPresenterSelector mPresenterSelector; private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>(); @@ -107,7 +109,7 @@ public class DvrBrowseFragment extends BrowseFragment implements public int compare(Object lhs, Object rhs) { if (lhs instanceof ScheduledRecording) { if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); } else { return -1; @@ -120,36 +122,15 @@ public class DvrBrowseFragment extends BrowseFragment implements } }; - private final TvInputCallback mTvInputCallback = new TvInputCallback() { + private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = + new DvrScheduleManager.OnConflictStateChangeListener() { @Override - public void onInputAdded(String inputId) { - List<ScheduledRecording> scheduleRecordings = - mDvrDataManager.getScheduledRecordings(inputId); - if (!scheduleRecordings.isEmpty()) { - onScheduledRecordingStatusChanged(ScheduledRecording.toArray(scheduleRecordings)); - } - handleSeriesRecordingsChanged(mDvrDataManager.getSeriesRecordings(inputId)); - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (TextUtils.equals(recordedProgram.getInputId(), inputId)) { - handleRecordedProgramChanged(recordedProgram); + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { + if (mScheduleAdapter != null) { + for (ScheduledRecording schedule : schedules) { + onScheduledRecordingStatusChanged(schedule); } } - postUpdateRows(); - } - - @Override - public void onInputRemoved(String inputId) { - List<ScheduledRecording> scheduleRecordings = - mDvrDataManager.getScheduledRecordings(inputId); - onScheduledRecordingRemoved( - scheduleRecordings.toArray(new ScheduledRecording[scheduleRecordings.size()])); - handleSeriesRecordingsRemoved(mDvrDataManager.getSeriesRecordings(inputId)); - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (TextUtils.equals(recordedProgram.getInputId(), inputId)) { - handleRecordedProgramRemoved(recordedProgram); - } - } - postUpdateRows(); } }; @@ -165,8 +146,9 @@ public class DvrBrowseFragment extends BrowseFragment implements if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); Context context = getContext(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrScheudleManager = singletons.getDvrScheduleManager(); mPresenterSelector = new ClassPresenterSelector() .addClassPresenter(ScheduledRecording.class, new ScheduledRecordingPresenter(context)) @@ -177,7 +159,7 @@ public class DvrBrowseFragment extends BrowseFragment implements mGenreLabels.add(getString(R.string.dvr_main_others)); setupUiElements(); setupAdapters(); - mTvInputManagerHelper.addCallback(mTvInputCallback); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); prepareEntranceTransition(); if (mDvrDataManager.isInitialized()) { startEntranceTransition(); @@ -194,9 +176,8 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); mHandler.removeCallbacks(mUpdateRowsRunnable); - mTvInputManagerHelper.removeCallback(mTvInputCallback); + mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); mDvrDataManager.removeRecordedProgramListener(this); mDvrDataManager.removeScheduledRecordingListener(this); mDvrDataManager.removeSeriesRecordingListener(this); @@ -204,6 +185,12 @@ public class DvrBrowseFragment extends BrowseFragment implements mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); mRowsAdapter.clear(); mSeriesId2LatestProgram.clear(); + for (Presenter presenter : mPresenterSelector.getPresenters()) { + if (presenter instanceof DvrItemPresenter) { + ((DvrItemPresenter) presenter).unbindAllViewHolders(); + } + } + super.onDestroy(); } @Override @@ -221,9 +208,7 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onRecordedProgramLoadFinished() { for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (isInputExist(recordedProgram.getInputId())) { - handleRecordedProgramAdded(recordedProgram, true); - } + handleRecordedProgramAdded(recordedProgram, true); } updateRows(); if (mDvrDataManager.isInitialized()) { @@ -233,27 +218,27 @@ public class DvrBrowseFragment extends BrowseFragment implements } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramAdded(recordedProgram, true); - postUpdateRows(); } + postUpdateRows(); } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramChanged(recordedProgram); - postUpdateRows(); } + postUpdateRows(); } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramRemoved(recordedProgram); - postUpdateRows(); } + postUpdateRows(); } // No need to call updateRows() during ScheduledRecordings' change because @@ -320,7 +305,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private void setupAdapters() { mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new RecordedProgramAdapter(); + mSeriesAdapter = new SeriesAdapter(); for (int i = 0; i < mGenreAdapters.length; i++) { mGenreAdapters[i] = new RecordedProgramAdapter(); } @@ -330,9 +315,7 @@ public class DvrBrowseFragment extends BrowseFragment implements mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); // Recorded Programs. for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (isInputExist(recordedProgram.getInputId())) { - handleRecordedProgramAdded(recordedProgram, false); - } + 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. @@ -426,13 +409,11 @@ public class DvrBrowseFragment extends BrowseFragment implements private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { - if (isInputExist(seriesRecording.getInputId())) { - mSeriesAdapter.add(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.add(seriesRecording); - } + mSeriesAdapter.add(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.add(seriesRecording); } } } @@ -450,15 +431,13 @@ public class DvrBrowseFragment extends BrowseFragment implements private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { - if (isInputExist(seriesRecording.getInputId())) { - mSeriesAdapter.change(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - updateGenreAdapters(getGenreAdapters( - seriesRecording.getCanonicalGenreIds()), seriesRecording); - } else { - // Remove series recording from all genre rows if it has no recorded program - updateGenreAdapters(new ArrayList<>(), seriesRecording); - } + mSeriesAdapter.change(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + updateGenreAdapters(getGenreAdapters( + seriesRecording.getCanonicalGenreIds()), seriesRecording); + } else { + // Remove series recording from all genre rows if it has no recorded program + updateGenreAdapters(new ArrayList<>(), seriesRecording); } } } @@ -545,23 +524,18 @@ public class DvrBrowseFragment extends BrowseFragment implements } } - private boolean isInputExist(String inputId) { - return mTvInputManagerHelper.getTvInputInfo(inputId) != null; - } - private boolean needToShowScheduledRecording(ScheduledRecording recording) { int state = recording.getState(); - return isInputExist(recording.getInputId()) - && (state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED); + return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; } private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { RecordedProgram latestProgram = null; for (RecordedProgram program : mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { - if (isInputExist(program.getInputId()) && (latestProgram == null || RecordedProgram - .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0)) { + if (latestProgram == null || RecordedProgram + .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { latestProgram = program; } } @@ -583,6 +557,27 @@ public class DvrBrowseFragment extends BrowseFragment implements } } + private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> { + SeriesAdapter() { + super(mPresenterSelector, new Comparator<SeriesRecording>() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; + } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + } + }); + } + + @Override + public long getId(SeriesRecording item) { + return item.getId(); + } + } + private class RecordedProgramAdapter extends SortedArrayAdapter<Object> { RecordedProgramAdapter() { this(Integer.MAX_VALUE); diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java deleted file mode 100644 index 78f73fd5..00000000 --- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java +++ /dev/null @@ -1,65 +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.app.DialogFragment; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v17.leanback.widget.GuidanceStylist; -import android.support.v17.leanback.widget.GuidedAction; - -import com.android.tv.R; - -import java.util.List; - -/** - * A fragment which asks the user to cancel all series schedules recordings. - */ -public class DvrCancelAllSeriesRecordingFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL_ALL = 1; - private static final int ACTION_BACK = 2; - - @Override - public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_series_schedules_dialog_cancel_all); - Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete); - return new GuidanceStylist.Guidance(title, null, null, icon); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL_ALL) - .title(getResources().getString(R.string.dvr_series_schedules_cancel_all)) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_BACK) - .title(getResources().getString(R.string.dvr_series_schedules_dialog_back)) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - DvrSchedulesActivity activity = (DvrSchedulesActivity) getActivity(); - if (action.getId() == ACTION_CANCEL_ALL) { - activity.onCancelAllClicked(); - } - dismissDialog(); - } -} diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index fe65eebd..837d8ab2 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -26,7 +26,6 @@ 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.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; @@ -38,7 +37,6 @@ import java.util.concurrent.TimeUnit; public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragment { private final List<Long> mDurations = new ArrayList<>(); private Channel mChannel; - private Program mProgram; @Override public void onCreate(Bundle savedInstanceState) { diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java index b273c85c..806c775c 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import com.android.tv.R; +import com.android.tv.TvApplication; /** * Activity to show details view in DVR. @@ -69,6 +70,7 @@ public class DvrDetailsActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_details); long recordId = getIntent().getLongExtra(RECORDING_ID, -1); diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java index be995fcb..21f9c4b4 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java @@ -17,10 +17,14 @@ package com.android.tv.dvr.ui; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v17.leanback.app.DetailsFragment; @@ -36,11 +40,22 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; +import android.widget.Toast; import com.android.tv.R; +import com.android.tv.TvApplication; 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.parental.ParentalControlSettings; import com.android.tv.util.ImageLoader; +import com.android.tv.util.ToastUtils; +import com.android.tv.util.Utils; + +import java.io.File; abstract class DvrDetailsFragment extends DetailsFragment { private static final int LOAD_LOGO_IMAGE = 1; @@ -59,6 +74,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { } mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); setupAdapter(); + onCreateInternal(); } @Override @@ -74,8 +90,8 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void setupAdapter() { - DetailsOverviewRowPresenter rowPresenter = - new DetailsOverviewRowPresenter(new DetailsContentPresenter()); + DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( + new DetailsContentPresenter(getActivity())); rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, null)); rowPresenter.setSharedElementEnterTransition(getActivity(), @@ -105,13 +121,22 @@ abstract class DvrDetailsFragment extends DetailsFragment { /** * Creates and returns presenter selector will be used by rows adaptor. */ - protected PresenterSelector onCreatePresenterSelector(DetailsOverviewRowPresenter rowPresenter) { + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); return presenterSelector; } /** + * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish + * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not + * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to + * do after the super class did onCreate, it should override this method and put the codes here. + */ + protected void onCreateInternal() { } + + /** * Updates actions of details overview. */ protected void updateActions() { @@ -198,6 +223,84 @@ abstract class DvrDetailsFragment extends DetailsFragment { } } + protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { + if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && + !isDataUriAccessible(recordedProgram.getDataUri())) { + // Since cleaning RecordedProgram from forgotten storage will take some time, + // ignore playback until cleaning is finished. + ToastUtils.show(getContext(), + getContext().getResources().getString(R.string.dvr_toast_recording_deleted), + Toast.LENGTH_SHORT); + return; + } + ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) + .getTvInputManagerHelper().getParentalControlSettings(); + if (!parental.isParentalControlsEnabled()) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); + if (channel != null && channel.isLocked()) { + checkPinToPlay(recordedProgram, seekTimeMs); + return; + } + String ratingString = recordedProgram.getContentRating(); + if (TextUtils.isEmpty(ratingString)) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + String[] ratingList = ratingString.split(","); + TvContentRating[] programRatings = new TvContentRating[ratingList.length]; + for (int i = 0; i < ratingList.length; i++) { + programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); + } + TvContentRating blockRatings = parental.getBlockedRating(programRatings); + if (blockRatings != null) { + checkPinToPlay(recordedProgram, seekTimeMs); + } else { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + } + } + + private boolean isDataUriAccessible(Uri dataUri) { + if (dataUri == null || dataUri.getPath() == null) { + return false; + } + try { + File recordedProgramPath = new File(dataUri.getPath()); + if (recordedProgramPath.exists()) { + return true; + } + } catch (SecurityException e) { + } + return false; + } + + private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + launchPlaybackActivity(recordedProgram, seekTimeMs, true); + } + } + }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + + private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, + boolean pinChecked) { + Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); + if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); + getActivity().startActivity(intent); + } + private static class MyImageLoaderCallback extends ImageLoader.ImageLoaderCallback<DvrDetailsFragment> { private final Context mContext; diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java index 6f287c70..73ddcdd0 100644 --- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java @@ -28,8 +28,6 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; 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 java.util.List; @@ -77,14 +75,7 @@ public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { return; } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - DvrDataManager dataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); - List<SeriesRecording> seriesRecordings = dataManager.getSeriesRecordings(mInputId); - for(SeriesRecording series : seriesRecordings) { - dvrManager.removeSeriesRecording(series.getId()); - } - List<ScheduledRecording> scheduledRecordings = dataManager.getScheduledRecordings(mInputId); - dvrManager.removeScheduledRecording(ScheduledRecording.toArray(scheduledRecordings)); - dvrManager.removeRecordedProgramByMissingStorage(mInputId); + dvrManager.forgetStorage(mInputId); Activity activity = getActivity(); if (activity instanceof DvrDetailsActivity) { // Since we removed everything, just finish the activity. diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index eaccd8ed..d26e6836 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -20,6 +20,7 @@ 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.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; @@ -30,9 +31,11 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; public class DvrGuidedStepFragment extends GuidedStepFragment { private DvrManager mDvrManager; + private OnActionClickListener mOnActionClickListener; protected DvrManager getDvrManager() { return mDvrManager; @@ -60,6 +63,14 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { return R.style.Theme_TV_Dvr_GuidedStep; } + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (mOnActionClickListener != null) { + mOnActionClickListener.onActionClick(action.getId()); + } + dismissDialog(); + } + protected void dismissDialog() { if (getActivity() instanceof MainActivity) { SafeDismissDialogFragment currentDialog = @@ -71,4 +82,8 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { ((DialogFragment) getParentFragment()).dismiss(); } } -} + + protected void setOnActionClickListener(OnActionClickListener listener) { + mOnActionClickListener = listener; + } +}
\ 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 50187a56..2b132db8 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -20,17 +20,21 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.ParcelableList; +import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; +import java.util.List; + public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** * Key for input ID. @@ -43,11 +47,6 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { */ public static final String KEY_PROGRAM = "DvrHalfSizedDialogFragment.program"; /** - * Key for the programs. - * Type: {@link ParcelableList}. - */ - public static final String KEY_PROGRAMS = "DvrHalfSizedDialogFragment.programs"; - /** * Key for the channel ID. * Type: long. */ @@ -90,23 +89,35 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } public abstract static class DvrGuidedStepDialogFragment extends DvrHalfSizedDialogFragment { + private DvrGuidedStepFragment mFragment; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - GuidedStepFragment fragment = onCreateGuidedStepFragment(); - fragment.setArguments(getArguments()); - GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host); + mFragment = onCreateGuidedStepFragment(); + mFragment.setArguments(getArguments()); + mFragment.setOnActionClickListener(getOnActionClickListener()); + GuidedStepFragment.add(getChildFragmentManager(), + mFragment, R.id.halfsized_dialog_host); return view; } - protected abstract GuidedStepFragment onCreateGuidedStepFragment(); + @Override + public void setOnActionClickListener(OnActionClickListener listener) { + super.setOnActionClickListener(listener); + if (mFragment != null) { + mFragment.setOnActionClickListener(listener); + } + } + + protected abstract DvrGuidedStepFragment onCreateGuidedStepFragment(); } /** A dialog fragment for {@link DvrScheduleFragment}. */ public static class DvrScheduleDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrScheduleFragment(); } } @@ -114,7 +125,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrProgramConflictFragment}. */ public static class DvrProgramConflictDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrProgramConflictFragment(); } } @@ -122,7 +133,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrChannelWatchConflictFragment}. */ public static class DvrChannelWatchConflictDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrChannelWatchConflictFragment(); } } @@ -131,7 +142,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrChannelRecordDurationOptionDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrChannelRecordDurationOptionFragment(); } } @@ -140,7 +151,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrInsufficientSpaceErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrInsufficientSpaceErrorFragment(); } } @@ -149,15 +160,52 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrMissingStorageErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrMissingStorageErrorFragment(); } } + /** + * A dialog fragment to show error message when the current storage is too small to + * support DVR + */ + public static class DvrSmallSizedStorageErrorDialogFragment + extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrGuidedStepFragment() { + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString( + R.string.dvr_error_small_sized_storage_title); + String description = getResources().getString( + R.string.dvr_error_small_sized_storage_description, + DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024 + / 1024 / 1024); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(GuidedAction.ACTION_ID_OK) + .title(android.R.string.ok) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + dismissDialog(); + } + }; + } + } + /** A dialog fragment for {@link DvrStopRecordingFragment}. */ public static class DvrStopRecordingDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrStopRecordingFragment(); } } @@ -165,7 +213,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrAlreadyScheduledFragment}. */ public static class DvrAlreadyScheduledDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrAlreadyScheduledFragment(); } } @@ -173,8 +221,8 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrAlreadyRecordedFragment}. */ public static class DvrAlreadyRecordedDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrAlreadyRecordedFragment(); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java new file mode 100644 index 00000000..339e5d2f --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrItemPresenter.java @@ -0,0 +1,80 @@ +/* + * 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.support.annotation.CallSuper; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.View.OnClickListener; + +import com.android.tv.dvr.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}. + */ +public abstract class DvrItemPresenter extends Presenter { + private final Set<ViewHolder> mBoundViewHolders = new HashSet<>(); + private final OnClickListener mOnClickListener = onCreateOnClickListener(); + + @Override + @CallSuper + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + viewHolder.view.setTag(o); + viewHolder.view.setOnClickListener(mOnClickListener); + mBoundViewHolders.add(viewHolder); + } + + @Override + @CallSuper + public void onUnbindViewHolder(ViewHolder viewHolder) { + mBoundViewHolders.remove(viewHolder); + } + + /** + * Unbinds all bound view holders. + */ + public void unbindAllViewHolders() { + // When browse fragments are destroyed, RecyclerView would not call presenters' + // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. + for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { + onUnbindViewHolder(viewHolder); + } + } + + /** + * Creates {@link OnClickListener} for DVR library's card views. + */ + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof RecordingCardView) { + RecordingCardView v = (RecordingCardView) view; + DvrUiHelper.startDetailsActivity((Activity) v.getContext(), + v.getTag(), v.getImageView(), false); + } + } + }; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java index 7be92f1e..8c4c856c 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; @@ -36,8 +37,6 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { private static final String TAG = "DvrPlaybackCardPresenter"; private static final boolean DEBUG = false; - private int mSelectedBackgroundColor = -1; - private int mDefaultBackgroundColor = -1; private final int mRelatedRecordingCardWidth; private final int mRelatedRecordingCardHeight; @@ -58,12 +57,17 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { } @Override - public void onClick(View v) { - long programId = (long) v.getTag(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + long programId = ((RecordedProgram) v.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + }; } @Override diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java index 1a3ae43c..0bc4ecb1 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java @@ -126,6 +126,13 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (mReadyToControl) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN + && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING)) { + // Workaround of b/31489271. Clicks play/pause button first to reset play controls + // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. + onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + } return super.onKey(v, keyCode, event); } return false; @@ -134,10 +141,7 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public boolean hasValidMedia() { PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return false; - } - return true; + return playbackState != null; } @Override diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java index 9184f4f7..51ec93b8 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java @@ -33,6 +33,7 @@ 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; import android.view.Display; import android.view.View; import android.view.ViewGroup; @@ -42,6 +43,7 @@ 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; @@ -63,7 +65,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private DvrPlaybackMediaSessionHelper mMediaSessionHelper; private DvrPlaybackControlHelper mPlaybackControlHelper; private ArrayObjectAdapter mRowsAdapter; - private ArrayObjectAdapter mRelatedRecordingsRowAdapter; + private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; private DvrDataManager mDvrDataManager; private ContentRatingsManager mContentRatingsManager; private TvView mTvView; @@ -108,7 +111,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView)); + getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); setUpRows(); preparePlayback(getActivity().getIntent()); @@ -166,9 +169,10 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); mPlaybackControlHelper.unregisterCallback(); mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); } /** @@ -196,6 +200,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { updateAspectRatio(mAppliedAspectRatio); } + public RecordedProgram getNextEpisode(RecordedProgram program) { + int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); + if (position == mRelatedRecordingsRowAdapter.size()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + void onMediaControllerUpdated() { mRowsAdapter.notifyArrayItemRangeChanged(0, 1); } @@ -261,8 +274,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } private ListRow getRelatedRecordingsRow() { - mRelatedRecordingsRowAdapter = - new ArrayObjectAdapter(new DvrPlaybackCardPresenter(getActivity())); + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); HeaderItem header = new HeaderItem(0, getActivity().getString(R.string.dvr_playback_related_recordings)); return new ListRow(header, mRelatedRecordingsRowAdapter); @@ -277,4 +290,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, TvInputManager.TIME_SHIFT_INVALID_TIME); } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + long getId(BaseProgram item) { + return item.getId(); + } + } }
\ 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 a907b198..da6d1637 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.annotation.TargetApi; -import android.app.ProgressDialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; @@ -27,7 +26,6 @@ import android.support.v17.leanback.app.GuidedStepFragment; 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; @@ -37,10 +35,10 @@ 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.SeriesRecordingScheduler.ProgramLoadCallback; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; +import java.util.Collections; import java.util.List; /** @@ -50,6 +48,8 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { + private static final String TAG = "DvrScheduleFragment"; + private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; @@ -62,8 +62,12 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic() - && dvrManager.getSeriesRecording(mProgram) == null); + SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, + "The program should be episodic: " + mProgram); + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(mProgram); + SoftPreconditions.checkArgument(seriesRecording == null + || seriesRecording.isStopped(), TAG, + "The series recording should be stopped or null: " + seriesRecording); super.onCreate(savedInstanceState); } @@ -122,19 +126,22 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { R.id.halfsized_dialog_host); } } else if (action.getId() == ACTION_RECORD_SERIES) { - ProgressDialog dialog = ProgressDialog.show(getContext(), null, - getString(R.string.dvr_schedule_progress_message_reading_programs)); - getDvrManager().queryProgramsForSeries(mProgram, new ProgramLoadCallback() { - @Override - public void onProgramLoadFinished(@NonNull List<Program> programs) { - dialog.dismiss(); - // TODO: Create series recording in series settings fragment. - SeriesRecording seriesRecording = - getDvrManager().addSeriesRecording(mProgram, programs); - DvrUiHelper.startSeriesSettingsActivity(getContext(), seriesRecording.getId()); - dismissDialog(); - } - }); + SeriesRecording seriesRecording = TvApplication.getSingletons(getContext()) + .getDvrDataManager().getSeriesRecording(mProgram.getSeriesId()); + if (seriesRecording == null) { + seriesRecording = getDvrManager().addSeriesRecording(mProgram, + Collections.emptyList(), SeriesRecording.STATE_SERIES_STOPPED); + } else { + // Reset priority to the highest. + seriesRecording = SeriesRecording.buildFrom(seriesRecording) + .setPriority(TvApplication.getSingletons(getContext()) + .getDvrScheduleManager().suggestNewSeriesPriority()) + .build(); + getDvrManager().updateSeriesRecording(seriesRecording); + } + DvrUiHelper.startSeriesSettingsActivity(getContext(), + seriesRecording.getId(), null, true, true, true); + dismissDialog(); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java index 316cb381..f6e6ac26 100644 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java @@ -17,16 +17,24 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.app.ProgressDialog; import android.os.Bundle; import android.support.annotation.IntDef; -import android.support.v17.leanback.app.DetailsFragment; 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 java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Activity to show the list of recording schedules. @@ -49,46 +57,48 @@ public class DvrSchedulesActivity extends Activity { * A type which means the activity will display a scheduled recording list of a series * recording. */ - public final static int TYPE_SERIES_SCHEDULE = 1; - - private Runnable mCancelAllClickedRunnable; + public static final int TYPE_SERIES_SCHEDULE = 1; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onCreate(final Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + // Pass null to prevent automatically re-creating fragments + super.onCreate(null); setContentView(R.layout.activity_dvr_schedules); - if (savedInstanceState == null) { - int schedulesType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); - DetailsFragment schedulesFragment = null; - if (schedulesType == TYPE_FULL_SCHEDULE) { - schedulesFragment = new DvrSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - } else if (schedulesType == TYPE_SERIES_SCHEDULE) { - schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - } - if (schedulesFragment != null) { - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } else { - finish(); - } - } - } - - /** - * Sets cancel all runnable which will implement operations after clicking cancel all dialog. - */ - public void setCancelAllClickedRunnable(Runnable cancelAllClickedRunnable) { - mCancelAllClickedRunnable = cancelAllClickedRunnable; - } - - /** - * Operations after clicking the cancel all. - */ - public void onCancelAllClicked() { - if (mCancelAllClickedRunnable != null) { - mCancelAllClickedRunnable.run(); + int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); + if (scheduleType == TYPE_FULL_SCHEDULE) { + DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); + schedulesFragment.setArguments(getIntent().getExtras()); + 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(); + } else { + finish(); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index ab695234..f57e4b05 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -37,6 +37,7 @@ public class DvrSeriesDeletionActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java new file mode 100644 index 00000000..1a0d13d3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java @@ -0,0 +1,48 @@ +/* + * 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.v17.leanback.app.GuidedStepFragment; + +import com.android.tv.R; + +public class DvrSeriesScheduledDialogActivity extends Activity { + /** + * Name of series recording id added to the Intent. + */ + public static final String SERIES_RECORDING_ID = "series_recording_id"; + + /** + * Name of flag to check if the dialog should show view schedule option. + */ + public static final String SHOW_VIEW_SCHEDULE_OPTION = "show_view_schedule_option"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.halfsized_dialog); + if (savedInstanceState == null) { + DvrSeriesScheduledFragment dvrSeriesScheduledFragment = + new DvrSeriesScheduledFragment(); + dvrSeriesScheduledFragment.setArguments(getIntent().getExtras()); + GuidedStepFragment.addAsRoot(this, dvrSeriesScheduledFragment, + R.id.halfsized_dialog_host); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java new file mode 100644 index 00000000..1173df46 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.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; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist; +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.dvr.DvrScheduleManager; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; + +import java.util.List; + +public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { + 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 int mSchedulesAddedCount = 0; + private boolean mHasConflict = false; + private int mInThisSeriesConflictCount = 0; + private int mOutThisSeriesConflictCount = 0; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + long seriesRecordingId = getArguments().getLong( + DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, SERIES_RECORDING_ID_NOT_SET); + if (seriesRecordingId == SERIES_RECORDING_ID_NOT_SET) { + getActivity().finish(); + return; + } + 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; + } + mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() + .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + List<ScheduledRecording> conflictingRecordings = + mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); + mHasConflict = !conflictingRecordings.isEmpty(); + for (ScheduledRecording recording : conflictingRecordings) { + if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { + ++mInThisSeriesConflictCount; + } else { + ++mOutThisSeriesConflictCount; + } + } + } + + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.dvr_series_recording_dialog_title); + Drawable icon; + if (!mHasConflict) { + icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null); + } else { + icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + } + return new GuidanceStylist.Guidance(title, getDescription(), null, icon); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Context context = getContext(); + actions.add(new GuidedAction.Builder(context) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + if (mShowViewScheduleOption) { + actions.add(new GuidedAction.Builder(context) + .id(ACTION_VIEW_SCHEDULES) + .title(R.string.dvr_action_view_schedules) + .build()); + } + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_VIEW_SCHEDULES) { + Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity + .TYPE_SERIES_SCHEDULE); + intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, + mSeriesRecording); + startActivity(intent); + } + getActivity().finish(); + } + + private String getDescription() { + if (!mHasConflict) { + return getResources().getQuantityString( + R.plurals.dvr_series_recording_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, + mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), + mInThisSeriesConflictCount + mOutThisSeriesConflictCount); + } else if (mInThisSeriesConflictCount != 0) { + 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, + mSchedulesAddedCount, mSchedulesAddedCount, + mSeriesRecording.getTitle()); + } else { + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_other_series_conflict, + 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 2af78081..3f7671b3 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,13 +17,14 @@ 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; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -31,22 +32,51 @@ import com.android.tv.ui.sidepanel.SettingsFragment; public class DvrSeriesSettingsActivity extends Activity { /** * Name of series id added to the Intent. + * Type: Long */ public static final String SERIES_RECORDING_ID = "series_recording_id"; + /** + * Name of the boolean flag to decide if the series recording with empty schedule and recording + * will be removed. + */ + 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. + */ + 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 + */ + public static final String CHANNEL_ID_LIST = "channel_id_list"; + + /** + * Name of the boolean flag to check if the confirm dialog should show view schedule option. + */ + public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = + "show_view_schedule_option_in_dialog"; @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); long seriesRecordingId = getIntent().getLongExtra(SERIES_RECORDING_ID, -1); SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(SeriesSettingsFragment.SERIES_RECORDING_ID, seriesRecordingId); SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); - settingFragment.setArguments(args); + settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } } -} + + @Override + public void onAttachedToWindow() { + if (!getIntent().getExtras().getBoolean(IS_WINDOW_TRANSLUCENT, true)) { + getWindow().setBackgroundDrawable( + new ColorDrawable(getColor(R.color.common_tv_background))); + } + } +}
\ 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 c0e21a18..c3867886 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -21,16 +21,22 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.support.annotation.IntDef; 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.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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -41,10 +47,33 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrStopRecordingFragment extends DvrGuidedStepFragment { - private static final int ACTION_STOP = 1; + /** + * The action ID for the stop action. + */ + public static final int ACTION_STOP = 1; + /** + * Key for the program. + * Type: {@link com.android.tv.data.Program}. + */ + public static final String KEY_REASON = "DvrStopRecordingFragment.type"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_USER_STOP, REASON_ON_CONFLICT}) + public @interface ReasonType {} + /** + * The dialog is shown because users want to stop some currently recording program. + */ + public static final int REASON_USER_STOP = 1; + /** + * The dialog is shown because users want to record some program that is conflict to the + * current recording program. + */ + public static final int REASON_ON_CONFLICT = 2; private ScheduledRecording mSchedule; private DvrDataManager mDvrDataManager; + private @ReasonType int mStopReason; + private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override @@ -85,6 +114,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { } mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); + mStopReason = args.getInt(KEY_REASON); } @Override @@ -99,7 +129,20 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.dvr_stop_recording_dialog_title); - String description = getString(R.string.dvr_stop_recording_dialog_description); + 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()); + } else { + description = getString(R.string.dvr_stop_recording_dialog_description); + } Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); return new Guidance(title, description, null, image); } @@ -115,12 +158,4 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { .clickAction(GuidedAction.ACTION_ID_CANCEL) .build()); } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_STOP) { - getDvrManager().stopRecording(mSchedule); - } - dismissDialog(); - } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java index d1cf57a6..5b880bd6 100644 --- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java @@ -26,16 +26,16 @@ import android.view.ViewGroup; import com.android.tv.R; /** - * A dialog fragment which contains {@link DvrCancelAllSeriesRecordingFragment}. + * A dialog fragment which contains {@link DvrStopSeriesRecordingFragment}. */ -public class DvrCancelAllSeriesRecordingDialogFragment extends DialogFragment { +public class DvrStopSeriesRecordingDialogFragment extends DialogFragment { public static final String DIALOG_TAG = "dialog_tag"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.halfsized_dialog, container, false); - GuidedStepFragment fragment = new DvrCancelAllSeriesRecordingFragment(); + GuidedStepFragment fragment = new DvrStopSeriesRecordingFragment(); fragment.setArguments(getArguments()); GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host); return view; diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java new file mode 100644 index 00000000..feaa2357 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -0,0 +1,104 @@ +/* + * 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.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.GuidedAction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.ApplicationSingletons; +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 java.util.ArrayList; +import java.util.List; + +/** + * A fragment which asks the user to stop series recording. + */ +public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { + /** + * Key for the series recording to be stopped. + */ + public static final String KEY_SERIES_RECORDING = "key_series_recoridng"; + + private static final int ACTION_STOP_SERIES_RECORDING = 1; + + private SeriesRecording mSeriesRecording; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mSeriesRecording = getArguments().getParcelable(KEY_SERIES_RECORDING); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @NonNull + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.dvr_series_schedules_stop_dialog_title); + String description = getString(R.string.dvr_series_schedules_stop_dialog_description); + Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete); + return new GuidanceStylist.Guidance(title, description, null, icon); + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_STOP_SERIES_RECORDING) + .title(R.string.dvr_series_schedules_stop_dialog_action_stop) + .build()); + actions.add(new GuidedAction.Builder(activity) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_STOP_SERIES_RECORDING) { + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + DvrManager dvrManager = singletons.getDvrManager(); + DvrDataManager dataManager = singletons.getDvrDataManager(); + List<ScheduledRecording> toDelete = new ArrayList<>(); + for (ScheduledRecording r : dataManager.getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == mSeriesRecording.getId()) { + if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + toDelete.add(r); + } else { + dvrManager.stopRecording(r); + } + } + } + if (!toDelete.isEmpty()) { + dvrManager.forceRemoveScheduledRecording(ScheduledRecording.toArray(toDelete)); + } + dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) + .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + } + dismissDialog(); + } +} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java index fcf0925b..d320816e 100644 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java @@ -35,6 +35,8 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { 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 @@ -63,6 +65,16 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { } @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); @@ -77,4 +89,29 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { 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/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java index 9f78985f..158bd824 100644 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java @@ -52,7 +52,6 @@ public class PrioritySettingsFragment extends GuidedStepFragment { // button action's IDs are negative. private static final long ACTION_ID_SAVE = -100L; - private DvrDataManager mDvrDataManager; private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>(); private SeriesRecording mSelectedRecording; @@ -64,23 +63,23 @@ public class PrioritySettingsFragment extends GuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); mSeriesRecordings.clear(); mSeriesRecordings.add(new SeriesRecording.Builder() .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) .setPriority(Long.MAX_VALUE) .setId(ONE_TIME_RECORDING_ID) .build()); + DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); long comeFromSeriesRecordingId = getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); - for (SeriesRecording series : mDvrDataManager.getSeriesRecordings()) { + for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL || series.getId() == comeFromSeriesRecordingId) { mSeriesRecordings.add(series); } } mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); - mComeFromSeriesRecording = mDvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); + mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); mSelectedActionColor = diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java index 9eb7e385..e698b8a2 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java @@ -16,11 +16,8 @@ package com.android.tv.dvr.ui; -import android.content.Intent; import android.content.res.Resources; -import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; -import android.net.Uri; import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; @@ -30,40 +27,38 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; -import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.RecordedProgram; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.Utils; - -import java.io.File; /** * {@link DetailsFragment} for recorded program in DVR. */ -public class RecordedProgramDetailsFragment extends DvrDetailsFragment { +public class RecordedProgramDetailsFragment extends DvrDetailsFragment + implements DvrDataManager.RecordedProgramListener { private static final int ACTION_RESUME_PLAYING = 1; private static final int ACTION_PLAY_FROM_BEGINNING = 2; private static final int ACTION_DELETE_RECORDING = 3; private DvrWatchedPositionManager mDvrWatchedPositionManager; - private TvInputManagerHelper mTvInputManagerHelper; private RecordedProgram mRecordedProgram; private DetailsContent mDetailsContent; private boolean mPaused; + private DvrDataManager mDvrDataManager; @Override public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + mDvrDataManager.addRecordedProgramListener(this); super.onCreate(savedInstanceState); + } + + @Override + public void onCreateInternal() { mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) .getDvrWatchedPositionManager(); - mTvInputManagerHelper = TvApplication.getSingletons(getActivity()) - .getTvInputManagerHelper(); setDetailsOverviewRow(mDetailsContent); } @@ -83,10 +78,15 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { } @Override + public void onDestroy() { + mDvrDataManager.removeRecordedProgramListener(this); + super.onDestroy(); + } + + @Override protected boolean onLoadRecordingDetails(Bundle args) { long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecordedProgram = TvApplication.getSingletons(getActivity()).getDvrDataManager() - .getRecordedProgram(recordedProgramId); + mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); if (mRecordedProgram == null) { // notify super class to end activity before initializing anything return false; @@ -114,8 +114,8 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); - if (mDvrWatchedPositionManager.getWatchedPosition(mRecordedProgram.getId()) - != TvInputManager.TIME_SHIFT_INVALID_TIME) { + if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, res.getString(R.string.dvr_detail_resume_play), null, res.getDrawable(R.drawable.lb_ic_play))); @@ -139,9 +139,9 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { @Override public void onActionClicked(Action action) { if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { - startPlayback(TvInputManager.TIME_SHIFT_INVALID_TIME); + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); } else if (action.getId() == ACTION_RESUME_PLAYING) { - startPlayback(mDvrWatchedPositionManager + startPlayback(mRecordedProgram, mDvrWatchedPositionManager .getWatchedPosition(mRecordedProgram.getId())); } else if (action.getId() == ACTION_DELETE_RECORDING) { DvrManager dvrManager = TvApplication @@ -153,66 +153,18 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { }; } - private boolean isDataUriAccessible(Uri dataUri) { - if (dataUri == null || dataUri.getPath() == null) { - return false; - } - try { - File recordedProgramPath = new File(dataUri.getPath()); - if (recordedProgramPath.exists()) { - return true; - } - } catch (SecurityException e) { - } - return false; - } + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } - private void startPlayback(long seekTimeMs) { - if (Utils.isInBundledPackageSet(mRecordedProgram.getPackageName()) - && !isDataUriAccessible(mRecordedProgram.getDataUri())) { - // Currently missing storage is handled only for TunerTvInput. - DvrUiHelper.showDvrMissingStorageErrorDialog(getActivity(), - mRecordedProgram.getInputId()); - return; - } - ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings(); - if (!parental.isParentalControlsEnabled()) { - launchPlaybackActivity(seekTimeMs, false); - return; - } - String ratingString = mRecordedProgram.getContentRating(); - if (TextUtils.isEmpty(ratingString)) { - launchPlaybackActivity(seekTimeMs, false); - return; - } - String[] ratingList = ratingString.split(","); - TvContentRating[] programRatings = new TvContentRating[ratingList.length]; - for (int i = 0; i < ratingList.length; i++) { - programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); - } - TvContentRating blockRatings = parental.getBlockedRating(programRatings); - if (blockRatings != null) { - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - launchPlaybackActivity(seekTimeMs, true); - } - } - }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } else { - launchPlaybackActivity(seekTimeMs, false); - } - } + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } - private void launchPlaybackActivity(long seekTimeMs, boolean pinChecked) { - Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); - if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (recordedProgram.getId() == mRecordedProgram.getId()) { + getActivity().finish(); + } } - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); - getActivity().startActivity(intent); } } diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java index 704d3a3f..1bf34310 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java @@ -21,13 +21,11 @@ import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; import android.net.Uri; -import android.support.v17.leanback.widget.Presenter; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; @@ -35,7 +33,6 @@ 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.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; import com.android.tv.util.Utils; @@ -45,7 +42,7 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. */ -public class RecordedProgramPresenter extends Presenter implements OnClickListener { +public class RecordedProgramPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrWatchedPositionManager mDvrWatchedPositionManager; private final Context mContext; @@ -108,20 +105,16 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen public void onBindViewHolder(ViewHolder viewHolder, Object o) { final RecordedProgram program = (RecordedProgram) o; final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.setTag(program); Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - SpannableString title; - if (mShowEpisodeTitle) { - title = new SpannableString(program.getEpisodeDisplayTitle(mContext)); - } else { - String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(mContext); - title = titleWithEpisodeNumber == null ? null - : new SpannableString(titleWithEpisodeNumber); - } + String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) + : program.getTitleWithEpisodeNumber(mContext); + SpannableString title = titleString == null ? null : new SpannableString(titleString); if (TextUtils.isEmpty(title)) { title = new SpannableString(channel != null ? channel.getDisplayName() : mContext.getResources().getString(R.string.no_program_information)); } else if (!mShowEpisodeTitle) { + // TODO: Some translation may add delimiters in-between program titles, we should use + // a more robust way to get the span range. String programTitle = program.getTitle(); title.setSpan(new TextAppearanceSpan(mContext, R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 @@ -144,7 +137,6 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen String durationString = getContext().getResources().getQuantityString( R.plurals.dvr_program_duration, durationMinutes, durationMinutes); cardView.setContent(getDescription(program), durationString); - viewHolder.view.setOnClickListener(this); if (viewHolder instanceof RecordedProgramViewHolder) { RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; cardViewHolder.setProgram(program); @@ -152,6 +144,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen cardViewHolder .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); } + super.onBindViewHolder(viewHolder, o); } @Override @@ -161,14 +154,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); } ((RecordingCardView) viewHolder.view).reset(); - } - - @Override - public void onClick(View v) { - if (v instanceof RecordingCardView) { - DvrUiHelper.startDetailsActivity((Activity) mContext, (RecordedProgram) v.getTag(), - ((RecordingCardView) v).getImageView()); - } + super.onUnbindViewHolder(viewHolder); } /** diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java index fa4562fd..51c3b03b 100644 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/RecordingCardView.java @@ -48,6 +48,8 @@ class RecordingCardView extends BaseCardView { private final TextView mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; + private final View mAffiliatedIconContainer; + private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; RecordingCardView(Context context) { @@ -71,6 +73,8 @@ class RecordingCardView extends BaseCardView { mImageWidth = imageWidth; mImageHeight = imageHeight; 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); @@ -138,6 +142,15 @@ class RecordingCardView extends BaseCardView { } } + public void setAffiliatedIcon(int imageResId) { + if (imageResId > 0) { + mAffiliatedIconContainer.setVisibility(View.VISIBLE); + mAffiliatedIcon.setImageResource(imageResId); + } else { + mAffiliatedIconContainer.setVisibility(View.INVISIBLE); + } + } + /** * Returns image view. */ diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java index 2271d932..4e19ec3f 100644 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java @@ -33,13 +33,10 @@ import com.android.tv.dvr.ScheduledRecording; */ abstract class RecordingDetailsFragment extends DvrDetailsFragment { private ScheduledRecording mRecording; - private DetailsContent mDetailsContent; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mDetailsContent = createDetailsContent(); - setDetailsOverviewRow(mDetailsContent); + protected void onCreateInternal() { + setDetailsOverviewRow(createDetailsContent()); } @Override @@ -47,11 +44,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment { long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() .getScheduledRecording(scheduledRecordingId); - if (mRecording == null) { - // notify super class to end activity before initializing anything - return false; - } - return true; + return mRecording != null; } /** diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java index 5c1ba48c..60816bb5 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java @@ -89,7 +89,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment private int getScheduleIconId() { if (mDvrManager.isConflicting(getRecording())) { - return R.drawable.ic_warning_white_36dp; + return R.drawable.ic_warning_white_32dp; } else { return R.drawable.ic_schedule_32dp; } diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java index 1f67bbe3..5f447f13 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java @@ -20,12 +20,10 @@ import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.os.Handler; -import android.support.v17.leanback.widget.Presenter; 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.ApplicationSingletons; @@ -33,7 +31,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.Utils; @@ -42,10 +40,11 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -public class ScheduledRecordingPresenter extends Presenter { +public class ScheduledRecordingPresenter extends DvrItemPresenter { private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); private final ChannelDataManager mChannelDataManager; + private final DvrManager mDvrManager; private final Context mContext; private final int mProgressBarColor; @@ -95,6 +94,7 @@ public class ScheduledRecordingPresenter extends Presenter { public ScheduledRecordingPresenter(Context context) { ApplicationSingletons singletons = TvApplication.getSingletons(context); mChannelDataManager = singletons.getChannelDataManager(); + mDvrManager = singletons.getDvrManager(); mContext = context; mProgressBarColor = context.getResources() .getColor(R.color.play_controls_recording_icon_color_on_focus); @@ -129,19 +129,15 @@ public class ScheduledRecordingPresenter extends Presenter { cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), recording.getStartTimeMs(), false, true, false, 0), null); } + if (mDvrManager.isConflicting(recording)) { + cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); + } else { + cardView.setAffiliatedIcon(0); + } viewHolder.updateProgressBar(); - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (v instanceof RecordingCardView) { - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), recording, - ((RecordingCardView) v).getImageView(), false); - } - } - }; - baseHolder.view.setOnClickListener(clickListener); viewHolder.mScheduledRecording = recording; viewHolder.startUpdateProgressBar(); + super.onBindViewHolder(viewHolder, o); } @Override @@ -151,6 +147,7 @@ public class ScheduledRecordingPresenter extends Presenter { final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.mScheduledRecording = null; cardView.reset(); + super.onUnbindViewHolder(viewHolder); } private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java index c29d62ae..36e3cfc1 100644 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java @@ -37,6 +37,7 @@ import com.android.tv.dvr.RecordedProgram; import com.android.tv.dvr.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -145,17 +146,19 @@ public class SeriesDeletionFragment extends GuidedStepFragment { public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_DELETE) { - int deletionCount = 0; - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + List<Long> idsToDelete = new ArrayList<>(); for (GuidedAction guidedAction : getActions()) { if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID && guidedAction.isChecked()) { - dvrManager.removeRecordedProgram(guidedAction.getId()); - deletionCount++; + idsToDelete.add(guidedAction.getId()); } } + if (!idsToDelete.isEmpty()) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedPrograms(idsToDelete); + } Toast.makeText(getContext(), getResources().getQuantityString( - R.plurals.dvr_msg_episodes_deleted, deletionCount, deletionCount, + R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), mRecordings.size()), Toast.LENGTH_LONG).show(); finishGuidedStepFragments(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java index 0156e9d9..e9e391d4 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java @@ -17,6 +17,8 @@ package com.android.tv.dvr.ui; import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.media.tv.TvInputManager; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import android.support.v17.leanback.widget.Action; @@ -37,8 +39,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 java.util.Collections; @@ -50,24 +55,44 @@ import java.util.List; */ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { - private static final int ACTION_SERIES_SCHEDULES = 1; - private static final int ACTION_DELETE = 2; + private static final int ACTION_WATCH = 1; + private static final int ACTION_SERIES_SCHEDULES = 2; + private static final int ACTION_DELETE = 3; + private DvrWatchedPositionManager mDvrWatchedPositionManager; private DvrDataManager mDvrDataManager; private SeriesRecording mSeries; // NOTICE: mRecordedPrograms should only be used in creating details fragments. // After fragments are created, it should be cleared to save resources. private List<RecordedProgram> mRecordedPrograms; + private RecordedProgram mRecommendRecordedProgram; private DetailsContent mDetailsContent; private int mSeasonRowCount; private SparseArrayObjectAdapter mActionsAdapter; private Action mDeleteAction; + private boolean mPaused; + private long mInitialPlaybackPositionMs; + private String mWatchLabel; + private String mResumeLabel; + private Drawable mWatchDrawable; + private RecordedProgramPresenter mRecordedProgramPresenter; + @Override public void onCreate(Bundle savedInstanceState) { mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + 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); super.onCreate(savedInstanceState); + } + + @Override + protected void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); setDetailsOverviewRow(mDetailsContent); setupRecordedProgramsRow(); mDvrDataManager.addSeriesRecordingListener(this); @@ -76,6 +101,45 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement } @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateWatchAction(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + private void updateWatchAction() { + List<RecordedProgram> programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); + mRecommendRecordedProgram = getRecommendProgram(programs); + if (mRecommendRecordedProgram == null) { + mActionsAdapter.clear(ACTION_WATCH); + } else { + String episodeStatus; + if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + episodeStatus = mResumeLabel; + mInitialPlaybackPositionMs = mDvrWatchedPositionManager + .getWatchedPosition(mRecommendRecordedProgram.getId()); + } else { + episodeStatus = mWatchLabel; + mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( + getContext()); + mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, + episodeStatus, episodeDisplayNumber, mWatchDrawable)); + } + } + + @Override protected boolean onLoadRecordingDetails(Bundle args) { long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() @@ -114,6 +178,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement protected SparseArrayObjectAdapter onCreateActionsAdapter() { mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); + updateWatchAction(); mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, getString(R.string.dvr_detail_view_schedule), null, res.getDrawable(R.drawable.ic_schedule_32dp, null))); @@ -137,11 +202,13 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement super.onDestroy(); mDvrDataManager.removeSeriesRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); - if (mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED - && mDvrDataManager.getRecordedPrograms(mSeries.getId()).isEmpty()) { - TvApplication.getSingletons(getActivity()).getDvrManager() - .removeSeriesRecording(mSeries.getId()); + if (mSeries != null) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { + dvrManager.removeSeriesRecording(mSeries.getId()); + } } + mRecordedProgramPresenter.unbindAllViewHolders(); } @Override @@ -149,7 +216,9 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return new OnActionClickedListener() { @Override public void onActionClicked(Action action) { - if (action.getId() == ACTION_SERIES_SCHEDULES) { + if (action.getId() == ACTION_WATCH) { + startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); + } else if (action.getId() == ACTION_SERIES_SCHEDULES) { DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); } else if (action.getId() == ACTION_DELETE) { DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); @@ -158,6 +227,28 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement }; } + /** + * The programs are sorted by season number and episode number. + */ + private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { + for (int i = programs.size() - 1 ; i >= 0 ; i--) { + RecordedProgram program = programs.get(i); + int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { + continue; + } + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + return program; + } + if (i == programs.size() - 1) { + return program; + } else { + return programs.get(i + 1); + } + } + return programs.isEmpty() ? null : programs.get(0); + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } @@ -166,43 +257,57 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement for (SeriesRecording series : seriesRecordings) { if (mSeries.getId() == series.getId()) { mSeries = series; - // TODO: change action label. } } } @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeries.getId()) { + mSeries = null; + getActivity().finish(); + return; + } + } + } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - addProgram(recordedProgram); - if (mActionsAdapter.lookup(ACTION_DELETE) == null) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + addProgram(recordedProgram); + if (mActionsAdapter.lookup(ACTION_DELETE) == null) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } } } } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { // Do nothing } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); - if (row != null) { - SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); - adapter.remove(recordedProgram); - if (adapter.isEmpty()) { - getRowsAdapter().remove(row); - if (getRowsAdapter().size() == 1) { - // No season rows left. Only DetailsOverviewRow - mActionsAdapter.clear(ACTION_DELETE); + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); + if (row != null) { + SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); + adapter.remove(recordedProgram); + if (adapter.isEmpty()) { + getRowsAdapter().remove(row); + if (getRowsAdapter().size() == 1) { + // No season rows left. Only DetailsOverviewRow + mActionsAdapter.clear(ACTION_DELETE); + } } } + if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + updateWatchAction(); + } } } } @@ -224,7 +329,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { Object row = rowsAdaptor.get(i); if (row instanceof ListRow) { - int compareResult = RecordedProgram.numberCompare(seasonNumber, + int compareResult = BaseProgram.numberCompare(seasonNumber, ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); if (compareResult == 0) { return (ListRow) row; @@ -241,8 +346,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement : getString(R.string.dvr_detail_series_season_title, seasonNumber); HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(RecordedProgram.class, - new RecordedProgramPresenter(getContext(), true)); + selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); ListRow row = new ListRow(header, new SeasonRowAdapter(selector, new Comparator<RecordedProgram>() { @Override diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java index d2f26dd1..c2c0f596 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java @@ -18,24 +18,20 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.content.Context; -import android.content.Intent; import android.media.tv.TvContract; import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.widget.Presenter; -import android.support.v4.app.ActivityOptionsCompat; import android.text.TextUtils; -import android.view.View; import android.view.ViewGroup; -import com.android.tv.R; import com.android.tv.ApplicationSingletons; +import com.android.tv.R; import com.android.tv.TvApplication; 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.DvrDataManager.RecordedProgramListener; +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; @@ -45,11 +41,12 @@ import com.android.tv.dvr.SeriesRecording; import java.util.List; /** - * Presents a {@link SeriesRecording} in the {@link DvrBrowseFragment}. + * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ -public class SeriesRecordingPresenter extends Presenter { +public class SeriesRecordingPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrDataManager mDvrDataManager; + private final DvrManager mDvrManager; private final DvrWatchedPositionManager mWatchedPositionManager; private static final class SeriesRecordingViewHolder extends ViewHolder implements @@ -57,13 +54,15 @@ public class SeriesRecordingPresenter extends Presenter { private SeriesRecording mSeriesRecording; private RecordingCardView mCardView; private DvrDataManager mDvrDataManager; + private DvrManager mDvrManager; private DvrWatchedPositionManager mWatchedPositionManager; SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, - DvrWatchedPositionManager watchedPositionManager) { + DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { super(view); mCardView = view; mDvrDataManager = dvrDataManager; + mDvrManager = dvrManager; mWatchedPositionManager = watchedPositionManager; } @@ -96,27 +95,41 @@ public class SeriesRecordingPresenter extends Presenter { } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) { - mDvrDataManager.removeScheduledRecordingListener(this); - mWatchedPositionManager.addListener(this, recordedProgram.getId()); + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + mDvrDataManager.removeScheduledRecordingListener(this); + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { updateCardViewContent(); } } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + } + needToUpdateCardView = true; } + } + if (needToUpdateCardView) { updateCardViewContent(); } } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { // Do nothing } @@ -151,7 +164,7 @@ public class SeriesRecordingPresenter extends Presenter { List<RecordedProgram> recordedPrograms = mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); if (recordedPrograms.size() == 0) { - count = mDvrDataManager.getScheduledRecordings(mSeriesRecording.getId()).size(); + count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); quantityStringID = R.plurals.dvr_count_scheduled_recordings; } else { for (RecordedProgram recordedProgram : recordedPrograms) { @@ -176,6 +189,7 @@ public class SeriesRecordingPresenter extends Presenter { ApplicationSingletons singletons = TvApplication.getSingletons(context); mChannelDataManager = singletons.getChannelDataManager(); mDvrDataManager = singletons.getDvrDataManager(); + mDvrManager = singletons.getDvrManager(); mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); } @@ -183,7 +197,8 @@ public class SeriesRecordingPresenter extends Presenter { public ViewHolder onCreateViewHolder(ViewGroup parent) { Context context = parent.getContext(); RecordingCardView view = new RecordingCardView(context); - return new SeriesRecordingViewHolder(view, mDvrDataManager, mWatchedPositionManager); + return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, + mWatchedPositionManager); } @Override @@ -193,33 +208,14 @@ public class SeriesRecordingPresenter extends Presenter { final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.onBound(seriesRecording); setTitleAndImage(cardView, seriesRecording); - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - showSeriesRecordingDetails(v, seriesRecording); - } - }; - baseHolder.view.setOnClickListener(clickListener); - } - - private void showSeriesRecordingDetails(View view, SeriesRecording seriesRecording) { - if (view instanceof RecordingCardView) { - Context context = view.getContext(); - Intent intent = new Intent(context, DvrDetailsActivity.class); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, seriesRecording.getId()); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, - DvrDetailsActivity.SERIES_RECORDING_VIEW); - Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context, - ((RecordingCardView) view).getImageView(), - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); - context.startActivity(intent, bundle); - } + super.onBindViewHolder(baseHolder, o); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { ((RecordingCardView) viewHolder.view).reset(); ((SeriesRecordingViewHolder) viewHolder).onUnbound(); + super.onUnbindViewHolder(viewHolder); } private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java index c550935c..6c05c9c6 100644 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java @@ -17,45 +17,64 @@ 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; import com.android.tv.data.Channel; +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 java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Fragment for DVR series recording settings. */ public class SeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { - /** - * Name of series recording id added to the bundle. - * Type: Long - */ - public static final String SERIES_RECORDING_ID = "series_recording_id"; + private static final String TAG = "SeriesSettingsFragment"; + private static final boolean DEBUG = false; private static final long ACTION_ID_PRIORITY = 10; private static final long ACTION_ID_CHANNEL = 11; - private static final long SUB_ACTION_ID_CHANNEL_ONE = 101; private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; + // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id + private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; private DvrDataManager mDvrDataManager; + private ChannelDataManager mChannelDataManager; + private DvrManager mDvrManager; private SeriesRecording mSeriesRecording; - private Channel mChannel; private long mSeriesRecordingId; @ChannelOption int mChannelOption; + private Comparator<Channel> mChannelComparator; + private long mSelectedChannelId; + private int mBackStackCount; + private boolean mShowViewScheduleOptionInDialog; private String mFragmentTitle; private String mProrityActionTitle; @@ -63,6 +82,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment private String mProrityActionLowestText; private String mChannelsActionTitle; private String mChannelsActionAllText; + private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); + private List<Channel> mChannels = new ArrayList<>(); + private EpisodicProgramLoadTask mEpisodicProgramLoadTask; private GuidedAction mPriorityGuidedAction; private GuidedAction mChannelsGuidedAction; @@ -70,14 +92,49 @@ public class SeriesSettingsFragment extends GuidedStepFragment @Override public void onAttach(Context context) { super.onAttach(context); + mBackStackCount = getFragmentManager().getBackStackEntryCount(); mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mSeriesRecordingId = getArguments().getLong(SERIES_RECORDING_ID); + mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (mSeriesRecording == null) { + getActivity().finish(); + return; + } + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mShowViewScheduleOptionInDialog = getArguments().getBoolean( + DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); 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); + if (channel != null) { + mId2Channel.put(channel.getId(), channel); + mChannels.add(channel); + } + } + } mChannelOption = mSeriesRecording.getChannelOption(); - mChannel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(mSeriesRecording.getChannelId()); - // TODO: Handle when channel is null. + mSelectedChannelId = Channel.INVALID_ID; + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { + Channel channel = mChannelDataManager.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); 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); @@ -90,6 +147,22 @@ public class SeriesSettingsFragment extends GuidedStepFragment 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); + } + super.onDestroy(); } @Override @@ -108,19 +181,10 @@ public class SeriesSettingsFragment extends GuidedStepFragment updatePriorityGuidedAction(false); actions.add(mPriorityGuidedAction); - List<GuidedAction> channelSubActions = new ArrayList<GuidedAction>(); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ONE) - .title(mChannel.getDisplayText()) - .build()); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ALL) - .title(mChannelsActionAllText) - .build()); mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) .id(ACTION_ID_CHANNEL) .title(mChannelsActionTitle) - .subActions(channelSubActions) + .subActions(buildChannelSubAction()) .build(); actions.add(mChannelsGuidedAction); updateChannelsGuidedAction(false); @@ -140,13 +204,45 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == GuidedAction.ACTION_ID_OK) { - if (mChannelOption != mSeriesRecording.getChannelOption()) { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + mEpisodicProgramLoadTask = null; + } + if (mChannelOption != mSeriesRecording.getChannelOption() + || mSeriesRecording.isStopped() + || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mSeriesRecording.getChannelId() != mSelectedChannelId)) { + SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) + .setChannelOption(mChannelOption) + .setState(SeriesRecording.STATE_SERIES_NORMAL); + if (mSelectedChannelId != Channel.INVALID_ID) { + builder.setChannelId(mSelectedChannelId); + } TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) - .setChannelOption(mChannelOption) - .build()); + .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; + } + } + } + }); + } else { + showConfirmDialog(); } - finishGuidedStepFragments(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { finishGuidedStepFragments(); } else if (actionId == ACTION_ID_PRIORITY) { @@ -165,10 +261,12 @@ public class SeriesSettingsFragment extends GuidedStepFragment long actionId = action.getId(); if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + mSelectedChannelId = Channel.INVALID_ID; updateChannelsGuidedAction(true); return true; - } else if (actionId == SUB_ACTION_ID_CHANNEL_ONE) { + } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; + mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; updateChannelsGuidedAction(true); return true; } @@ -184,7 +282,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); } else { - mChannelsGuidedAction.setDescription(mChannel.getDisplayText()); + mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) + .getDisplayText()); } if (notifyActionChanged) { notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); @@ -210,13 +309,75 @@ public class SeriesSettingsFragment extends GuidedStepFragment } else if (priorityOrder >= totalSeriesCount - 1) { mPriorityGuidedAction.setDescription(mProrityActionLowestText); } else { - mPriorityGuidedAction.setDescription(Integer.toString(priorityOrder + 1)); + mPriorityGuidedAction.setDescription(getString( + R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } if (notifyActionChanged) { notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); } } + private void collectChannelsInBackground() { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + } + 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"); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + mEpisodicProgramLoadTask.execute(); + } + + private List<GuidedAction> buildChannelSubAction() { + List<GuidedAction> channelSubActions = new ArrayList<>(); + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ALL) + .title(mChannelsActionAllText) + .build()); + for (Channel channel : mChannels) { + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) + .title(channel.getDisplayText()) + .build()); + } + return channelSubActions; + } + + private void showConfirmDialog() { + DvrUiHelper.StartSeriesScheduledDialogActivity( + getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); + finishGuidedStepFragments(); + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 3a57d72e..393a5ff3 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -148,7 +148,10 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { return -1; } - private int findInsertPosition(T item) { + /** + * Finds the position that the given item should be inserted to keep the sorted order. + */ + public int findInsertPosition(T item) { for (int i = size() - mExtraItemCount - 1; i >=0; i--) { T r = (T) get(i); if (mComparator.compare(r, item) <= 0) { diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index 61de5764..d28f026c 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -22,12 +22,13 @@ import android.support.v17.leanback.widget.ClassPresenterSelector; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.widget.TextView; +import com.android.tv.ApplicationSingletons; 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; /** @@ -35,32 +36,39 @@ import com.android.tv.dvr.ScheduledRecording; */ public abstract class BaseDvrSchedulesFragment extends DetailsFragment implements DvrDataManager.ScheduledRecordingListener, - SchedulesHeaderRowPresenter.SchedulesHeaderRowListener, - ScheduleRowPresenter.ScheduleRowClickListener { + DvrScheduleManager.OnConflictStateChangeListener { /** * The key for scheduled recording which has be selected in the list. */ public static String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording"; - private SchedulesHeaderRowPresenter mHeaderRowPresenter; - private ScheduleRowPresenter mRowPresenter; private ScheduleRowAdapter mRowsAdapter; + private TextView mEmptyInfoScreenView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - mHeaderRowPresenter = onCreateHeaderRowPresenter(); - mHeaderRowPresenter.addListener(this); - mRowPresenter = onCreateRowPresenter(); - mRowPresenter.addListener(this); - presenterSelector.addClassPresenter(SchedulesHeaderRow.class, mHeaderRowPresenter); - presenterSelector.addClassPresenter(ScheduleRow.class, mRowPresenter); + presenterSelector.addClassPresenter(SchedulesHeaderRow.class, onCreateHeaderRowPresenter()); + presenterSelector.addClassPresenter(ScheduleRow.class, onCreateRowPresenter()); mRowsAdapter = onCreateRowsAdapter(presenterSelector); setAdapter(mRowsAdapter); mRowsAdapter.start(); - TvApplication.getSingletons(getContext()).getDvrDataManager() - .addScheduledRecordingListener(this); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrDataManager().addScheduledRecordingListener(this); + singletons.getDvrScheduleManager().addOnConflictStateChangeListener(this); + mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + int firstItemPosition = getFirstItemPosition(); + if (firstItemPosition != -1) { + getRowsFragment().setSelectedPosition(firstItemPosition, false); + } + return view; } /** @@ -73,34 +81,20 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment /** * Shows the empty message. */ - protected void showEmptyMessage(int message) { - TextView emptyInfoScreenView = (TextView) getActivity().findViewById( - R.id.empty_info_screen); - emptyInfoScreenView.setText(message); - emptyInfoScreenView.setVisibility(View.VISIBLE); + void showEmptyMessage(int messageId) { + mEmptyInfoScreenView.setText(messageId); + if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.VISIBLE); + } } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - // setSelectedPosition works only after the view is attached to a window. - view.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - int firstItemPosition = getFirstItemPosition(); - if (firstItemPosition != -1) { - setSelectedPosition(firstItemPosition, false); - } - view.getViewTreeObserver().removeOnWindowAttachListener(this); - } - - @Override - public void onWindowDetached() { - } - }); - return view; + /** + * Hides the empty message. + */ + void hideEmptyMessage() { + if (mEmptyInfoScreenView.getVisibility() == View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.GONE); + } } @Override @@ -112,10 +106,9 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onDestroy() { - TvApplication.getSingletons(getContext()).getDvrDataManager() - .removeScheduledRecordingListener(this); - mHeaderRowPresenter.removeListener(this); - mRowPresenter.removeListener(this); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrScheduleManager().removeOnConflictStateChangeListener(this); + singletons.getDvrDataManager().removeScheduledRecordingListener(this); mRowsAdapter.stop(); super.onDestroy(); } @@ -139,16 +132,6 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment * Gets the first focus position in schedules list. */ protected int getFirstItemPosition() { - Bundle args = getArguments(); - ScheduledRecording recording = null; - if (args != null) { - recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING); - } - final int selectedPostion = mRowsAdapter.indexOf( - mRowsAdapter.findRowByScheduledRecording(recording)); - if (selectedPostion != -1) { - return selectedPostion; - } for (int i = 0; i < mRowsAdapter.size(); i++) { if (mRowsAdapter.get(i) instanceof ScheduleRow) { return i; @@ -159,11 +142,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingAdded(recording); - } - if (mRowsAdapter != null) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { mRowsAdapter.onScheduledRecordingAdded(recording); } } @@ -171,11 +151,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingRemoved(recording); - } - if (mRowsAdapter != null) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { mRowsAdapter.onScheduledRecordingRemoved(recording); } } @@ -183,27 +160,19 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingUpdated(recording); - } - if (mRowsAdapter != null) { - mRowsAdapter.onScheduledRecordingUpdated(recording); + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { + mRowsAdapter.onScheduledRecordingUpdated(recording, false); } } } @Override - public void onUpdateAllScheduleRows() { - if (getRowsAdapter() != null) { - getRowsAdapter().notifyArrayItemRangeChanged(0, getRowsAdapter().size()); - } - } - - @Override - public void onDeleteClicked(ScheduleRow scheduleRow) { + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { if (mRowsAdapter != null) { - mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size()); + for (ScheduledRecording recording : schedules) { + mRowsAdapter.onScheduledRecordingUpdated(recording, true); + } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index f361ede3..722c9b6e 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,8 +18,12 @@ 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.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** @@ -48,4 +52,35 @@ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment { public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor) { return new ScheduleRowAdapter(getContext(), presenterSelecor); } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + super.onScheduledRecordingAdded(scheduledRecordings); + if (getRowsAdapter().size() > 0) { + hideEmptyMessage(); + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + super.onScheduledRecordingRemoved(scheduledRecordings); + if (getRowsAdapter().size() == 0) { + showEmptyMessage(R.string.dvr_schedules_empty_state); + } + } + + @Override + protected int getFirstItemPosition() { + Bundle args = getArguments(); + ScheduledRecording recording = null; + if (args != null) { + recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING); + } + final int selectedPostion = getRowsAdapter().indexOf( + getRowsAdapter().findRowByScheduledRecording(recording)); + if (selectedPostion != -1) { + return selectedPostion; + } + return super.getFirstItemPosition(); + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index ba8b0c36..42a1e72b 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -16,71 +16,154 @@ package com.android.tv.dvr.ui.list; +import android.annotation.TargetApi; +import android.database.ContentObserver; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.transition.Fade; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +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.DvrSchedulesActivity; -import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; +import java.util.List; + /** * A fragment to show the list of series schedule recordings. */ +@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. */ - public static String SERIES_SCHEDULES_KEY_SERIES_RECORDING = + 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. + */ + public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = + "series_schedules_key_series_programs"; + + private ChannelDataManager mChannelDataManager; + private SeriesRecording mSeriesRecording; + private List<Program> mPrograms; + private EpisodicProgramLoadTask mProgramLoadTask; + + private final SeriesRecordingListener mSeriesRecordingListener = + new SeriesRecordingListener() { + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + if (r.getId() == mSeriesRecording.getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + if (r.getId() == mSeriesRecording.getId() + && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { + ((SeriesScheduleRowAdapter) getRowsAdapter()) + .onSeriesRecordingUpdated(r); + return; + } + } + } + }; - private static String TAG = "DvrSeriesSchedulesFragment"; + 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 ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { } - private SeriesRecording mSeries; + @Override + public void onChannelListUpdated() { + executeProgramLoadingTask(); + } + + @Override + public void onChannelBrowsableChanged() { } + }; + + public DvrSeriesSchedulesFragment() { + setEnterTransition(new Fade(Fade.IN)); + } @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { - mSeries = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); + mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); + mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); } super.onCreate(savedInstanceState); - // "1" means there is only title row in series schedules list. So we should show an empty - // state info view. - if (getRowsAdapter().size() == 1) { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); - } - ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(new Runnable() { - @Override - public void run() { - SoftPreconditions.checkState(getRowsAdapter().get(0) instanceof - SeriesRecordingHeaderRow, TAG, "First row is not SchedulesHeaderRow"); - SeriesRecordingHeaderRow headerRow = - (SeriesRecordingHeaderRow) getRowsAdapter().get(0); - headerRow.setCancelAllChecked(true); - if (headerRow.getSeriesRecording() != null) { - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom( - headerRow.getSeriesRecording()).setState( - SeriesRecording.STATE_SERIES_CANCELED).build()); - } - onUpdateAllScheduleRows(); - } - }); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); + mChannelDataManager = singletons.getChannelDataManager(); + mChannelDataManager.addListener(mChannelListener); + getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, + mContentObserver); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + onProgramsUpdated(); return super.onCreateView(inflater, container, savedInstanceState); } + private void onProgramsUpdated() { + ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); + if (mPrograms == null || mPrograms.isEmpty()) { + showEmptyMessage(R.string.dvr_series_schedules_empty_state); + } else { + hideEmptyMessage(); + } + } + + @Override + public void onDestroy() { + if (mProgramLoadTask != null) { + mProgramLoadTask.cancel(true); + mProgramLoadTask = null; + } + getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mChannelDataManager.removeListener(mChannelListener); + TvApplication.getSingletons(getContext()).getDvrDataManager() + .removeSeriesRecordingListener(mSeriesRecordingListener); + super.onDestroy(); + } + @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { return new SeriesRecordingHeaderRowPresenter(getContext()); @@ -93,20 +176,33 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { @Override public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelector) { - return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeries); + return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeriesRecording); } @Override protected int getFirstItemPosition() { - if (mSeries != null && mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - return -1; + if (mSeriesRecording != null + && mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { + return 0; } return super.getFirstItemPosition(); } - @Override - public void onDestroy() { - ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(null); - super.onDestroy(); + private void executeProgramLoadingTask() { + if (mProgramLoadTask != null) { + mProgramLoadTask.cancel(true); + } + mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { + @Override + protected void onPostExecute(List<Program> programs) { + mPrograms = programs; + onProgramsUpdated(); + } + }; + mProgramLoadTask.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .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 new file mode 100644 index 00000000..23aebf59 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -0,0 +1,89 @@ +/* + * 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.list; + +import android.content.Context; + +import com.android.tv.data.Program; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording.Builder; + +/** + * A class for the episodic program. + */ +public class EpisodicProgramRow extends ScheduleRow { + private final String mInputId; + private final Program mProgram; + + public EpisodicProgramRow(String inputId, Program program, ScheduledRecording recording, + SchedulesHeaderRow headerRow) { + super(recording, headerRow); + mInputId = inputId; + mProgram = program; + } + + /** + * Returns the program. + */ + public Program getProgram() { + return mProgram; + } + + @Override + public long getChannelId() { + return mProgram.getChannelId(); + } + + @Override + public long getStartTimeMs() { + return mProgram.getStartTimeUtcMillis(); + } + + @Override + public long getEndTimeMs() { + return mProgram.getEndTimeUtcMillis(); + } + + @Override + public Builder createNewScheduleBuilder() { + return ScheduledRecording.builder(mInputId, mProgram); + } + + @Override + public String getProgramTitleWithEpisodeNumber(Context context) { + return mProgram.getTitleWithEpisodeNumber(context); + } + + @Override + public String getEpisodeDisplayTitle(Context context) { + return mProgram.getEpisodeDisplayTitle(context); + } + + @Override + public boolean matchSchedule(ScheduledRecording schedule) { + return schedule.getType() == ScheduledRecording.TYPE_PROGRAM + && mProgram.getId() == schedule.getProgramId(); + } + + @Override + public String toString() { + return super.toString() + + "(inputId=" + mInputId + + ",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 1e258d2d..3fc92e8a 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -16,54 +16,188 @@ package com.android.tv.dvr.ui.list; +import android.content.Context; +import android.support.annotation.Nullable; + +import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.ScheduledRecording; /** * A class for schedule recording row. */ public class ScheduleRow { - private ScheduledRecording mRecording; - private boolean mRemoveScheduleChecked; - private SchedulesHeaderRow mHeaderRow; + private final SchedulesHeaderRow mHeaderRow; + @Nullable private ScheduledRecording mSchedule; + private boolean mStopRecordingRequested; + private boolean mStartRecordingRequested; - public ScheduleRow(ScheduledRecording recording, SchedulesHeaderRow headerRow) { - mRecording = recording; - mRemoveScheduleChecked = false; + public ScheduleRow(@Nullable ScheduledRecording recording, SchedulesHeaderRow headerRow) { + mSchedule = recording; mHeaderRow = headerRow; } /** - * Sets scheduled recording. + * Gets which {@link SchedulesHeaderRow} this schedule row belongs to. */ - public void setRecording(ScheduledRecording recording) { - mRecording = recording; + public SchedulesHeaderRow getHeaderRow() { + return mHeaderRow; } /** - * Sets remove schedule checked status. + * Returns the recording schedule. */ - public void setRemoveScheduleChecked(boolean checked) { - mRemoveScheduleChecked = checked; + @Nullable + public ScheduledRecording getSchedule() { + return mSchedule; } /** - * Gets scheduled recording. + * Checks if the stop recording has been requested or not. */ - public ScheduledRecording getRecording() { - return mRecording; + public boolean isStopRecordingRequested() { + return mStopRecordingRequested; } /** - * Gets remove schedule checked status. + * Sets the flag of stop recording request. */ - public boolean isRemoveScheduleChecked() { - return mRemoveScheduleChecked; + public void setStopRecordingRequested(boolean stopRecordingRequested) { + SoftPreconditions.checkState(!mStartRecordingRequested); + mStopRecordingRequested = stopRecordingRequested; } /** - * Gets which {@link SchedulesHeaderRow} this schedule row belongs to. + * Checks if the start recording has been requested or not. */ - public SchedulesHeaderRow getHeaderRow() { - return mHeaderRow; + public boolean isStartRecordingRequested() { + return mStartRecordingRequested; + } + + /** + * Sets the flag of start recording request. + */ + public void setStartRecordingRequested(boolean startRecordingRequested) { + SoftPreconditions.checkState(!mStopRecordingRequested); + mStartRecordingRequested = startRecordingRequested; + } + + /** + * Sets the recording schedule. + */ + public void setSchedule(@Nullable ScheduledRecording schedule) { + mSchedule = schedule; + } + + /** + * Returns the channel ID. + */ + public long getChannelId() { + return mSchedule != null ? mSchedule.getChannelId() : -1; + } + + /** + * Returns the start time. + */ + public long getStartTimeMs() { + return mSchedule != null ? mSchedule.getStartTimeMs() : -1; + } + + /** + * Returns the end time. + */ + public long getEndTimeMs() { + return mSchedule != null ? mSchedule.getEndTimeMs() : -1; + } + + /** + * Returns the duration. + */ + public final long getDuration() { + return getEndTimeMs() - getStartTimeMs(); + } + + /** + * Checks if the program is on air. + */ + public final boolean isOnAir() { + long currentTimeMs = System.currentTimeMillis(); + return getStartTimeMs() <= currentTimeMs && getEndTimeMs() > currentTimeMs; + } + + /** + * Checks if the schedule is not started. + */ + public final boolean isRecordingNotStarted() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + /** + * Checks if the schedule is in progress. + */ + public final boolean isRecordingInProgress() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS; + } + + /** + * Checks if the schedule has been canceled or not. + */ + public final boolean isScheduleCanceled() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED; + } + + public boolean isRecordingFinished() { + return mSchedule != null + && (mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED); + } + + /** + * Creates and returns the new schedule with the existing information. + */ + public ScheduledRecording.Builder createNewScheduleBuilder() { + return mSchedule != null ? ScheduledRecording.buildFrom(mSchedule) : null; + } + + /** + * Returns the program title with episode number. + */ + public String getProgramTitleWithEpisodeNumber(Context context) { + return mSchedule != null ? mSchedule.getProgramTitleWithEpisodeNumber(context) : null; + } + + /** + * Returns the program title including the season/episode number. + */ + public String getEpisodeDisplayTitle(Context context) { + return mSchedule != null ? mSchedule.getEpisodeDisplayTitle(context) : null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "(schedule=" + mSchedule + + ",stopRecordingRequested=" + mStopRecordingRequested + + ",startRecordingRequested=" + mStartRecordingRequested + + ")"; + } + + /** + * Checks if the {@code schedule} is for the program or channel. + */ + public boolean matchSchedule(ScheduledRecording schedule) { + if (mSchedule == null) { + return false; + } + if (mSchedule.getType() == ScheduledRecording.TYPE_TIMED) { + return mSchedule.getChannelId() == schedule.getChannelId() + && mSchedule.getStartTimeMs() == schedule.getStartTimeMs() + && mSchedule.getEndTimeMs() == schedule.getEndTimeMs(); + } else { + return mSchedule.getProgramId() == schedule.getProgramId(); + } } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 3e2630c7..9cc82653 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -17,12 +17,19 @@ package com.android.tv.dvr.ui.list; import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.Log; 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.util.Utils; @@ -30,16 +37,34 @@ import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ public class ScheduleRowAdapter extends ArrayObjectAdapter { + private static final String TAG = "ScheduleRowAdapter"; + private static final boolean DEBUG = false; + private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final int MSG_UPDATE_ROW = 1; + private Context mContext; private final List<String> mTitles = new ArrayList<>(); + private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_ROW) { + long currentTimeMs = System.currentTimeMillis(); + handleUpdateRow(currentTimeMs); + sendNextUpdateMessage(currentTimeMs); + } + } + }; public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) { super(classPresenterSelector); @@ -64,7 +89,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { .getDvrDataManager().getNonStartedScheduledRecordings(); recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager() .getStartedRecordings()); - Collections.sort(recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + Collections.sort(recordingList, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis()); for (int i = 0; i < recordingList.size();) { ArrayList<ScheduledRecording> section = new ArrayList<>(); @@ -83,6 +109,7 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { } deadLine += ONE_DAY_MS; } + sendNextUpdateMessage(System.currentTimeMillis()); } private String calculateHeaderDate(long deadLine) { @@ -93,8 +120,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { headerDate = mTitles.get(titleIndex); } else { headerDate = DateUtils.formatDateTime(getContext(), deadLine, - DateUtils.FORMAT_SHOW_WEEKDAY| DateUtils.FORMAT_ABBREV_WEEKDAY - | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH); + DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH); } return headerDate; } @@ -103,13 +130,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { * Stops schedules row adapter. */ public void stop() { - // TODO: Deal with other type of operation. + mHandler.removeCallbacksAndMessages(null); + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); for (int i = 0; i < size(); i++) { if (get(i) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(i); - if (scheduleRow.isRemoveScheduleChecked()) { - TvApplication.getSingletons(mContext).getDvrManager() - .removeScheduledRecording(scheduleRow.getRecording()); + ScheduleRow row = (ScheduleRow) get(i); + if (row.isScheduleCanceled()) { + dvrManager.removeScheduledRecording(row.getSchedule()); } } } @@ -124,8 +151,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { } for (int i = 0; i < size(); i++) { Object item = get(i); - if (item instanceof ScheduleRow) { - if (((ScheduleRow) item).getRecording().getId() == recording.getId()) { + if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { + if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { return (ScheduleRow) item; } } @@ -133,19 +160,32 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { return null; } - /** - * Adds a {@link ScheduleRow} by {@link ScheduledRecording} and update - * {@link SchedulesHeaderRow} information. - */ - protected void addScheduleRow(ScheduledRecording recording) { + private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (!(item instanceof ScheduleRow)) { + continue; + } + ScheduleRow row = (ScheduleRow) item; + if (row.getSchedule() != null && row.isStartRecordingRequested() + && row.matchSchedule(schedule)) { + return row; + } + } + return null; + } + + private void addScheduleRow(ScheduledRecording recording) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); if (recording != null) { int pre = -1; int index = 0; for (; index < size(); index++) { if (get(index) instanceof ScheduleRow) { ScheduleRow scheduleRow = (ScheduleRow) get(index); - if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare( - scheduleRow.getRecording(), recording) > 0) { + if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare( + scheduleRow.getSchedule(), recording) > 0) { break; } pre = index; @@ -157,11 +197,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { headerRow.setItemCount(headerRow.getItemCount() + 1); ScheduleRow addedRow = new ScheduleRow(recording, headerRow); add(++pre, addedRow); + updateHeaderDescription(headerRow); } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); headerRow.setItemCount(headerRow.getItemCount() + 1); ScheduleRow addedRow = new ScheduleRow(recording, headerRow); add(index, addedRow); + updateHeaderDescription(headerRow); } else { SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), mContext.getResources().getQuantityString( @@ -177,11 +219,11 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); } - /** - * Removes {@link ScheduleRow} and update {@link SchedulesHeaderRow} information. - */ - protected void removeScheduleRow(ScheduleRow scheduleRow) { + private void removeScheduleRow(ScheduleRow scheduleRow) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); if (scheduleRow != null) { + scheduleRow.setSchedule(null); SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); remove(scheduleRow); // Changes the count information of header which the removed row belongs to. @@ -191,58 +233,193 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { if (headerRow.getItemCount() == 0) { remove(headerRow); } else { - headerRow.setDescription(mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_section_subtitle, - headerRow.getItemCount(), headerRow.getItemCount())); replace(indexOf(headerRow), headerRow); + updateHeaderDescription(headerRow); } } } } + private void updateHeaderDescription(SchedulesHeaderRow headerRow) { + headerRow.setDescription(mContext.getResources().getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + headerRow.getItemCount(), headerRow.getItemCount())); + } + /** * Called when a schedule recording is added to dvr date manager. */ - public void onScheduledRecordingAdded(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - addScheduleRow(recording); - notifyArrayItemRangeChanged(0, size()); + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + ScheduleRow row = findRowWithStartRequest(schedule); + // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED + // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS. + // It happens in a short time and causes blinking. To avoid this intermediate state change, + // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS + // instead of in this method. + if (row == null) { + addScheduleRow(schedule); + sendNextUpdateMessage(System.currentTimeMillis()); } } /** * Called when a schedule recording is removed from dvr date manager. */ - public void onScheduledRecordingRemoved(ScheduledRecording recording) { - ScheduleRow scheduleRow = findRowByScheduledRecording(recording); - if (scheduleRow != null) { - removeScheduleRow(scheduleRow); + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); } - notifyArrayItemRangeChanged(0, size()); } /** * Called when a schedule recording is updated in dvr date manager. */ - public void onScheduledRecordingUpdated(ScheduledRecording recording) { - ScheduleRow scheduleRow = findRowByScheduledRecording(recording); - if (scheduleRow != null) { - scheduleRow.setRecording(recording); - if (!willBeKept(recording)) { - removeScheduleRow(scheduleRow); + public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + if (conflictChange && isStartOrStopRequested()) { + // Delay the conflict update until it gets the response of the start/stop request. + // The purpose is to avoid the intermediate conflict change. + addPendingUpdate(row); + return; + } + if (row.isStopRecordingRequested()) { + // Wait until the recording is finished + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + row.setStopRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + } + } else { + row.setSchedule(schedule); + if (!willBeKept(schedule)) { + removeScheduleRow(row); + } + } + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); + } else { + row = findRowWithStartRequest(schedule); + // When the start recording was requested, we give the highest priority. So it is + // guaranteed that the state will be changed from NOT_STARTED to the other state. + // Update the row with the next state not to show the intermediate state which causes + // blinking. + if (row != null + && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + // This can be called multiple times, so do not call + // ScheduleRow.setStartRecordingRequested(false) here. + row.setStartRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); } - } else if (willBeKept(recording)) { - addScheduleRow(recording); } - notifyArrayItemRangeChanged(0, size()); + } + + /** + * Checks if there is a row which requested start/stop recording. + */ + protected boolean isStartOrStopRequested() { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) { + return true; + } + } + } + return false; + } + + /** + * Delays update of the row. + */ + protected void addPendingUpdate(ScheduleRow row) { + mPendingUpdate.add(row); + } + + /** + * Executes the pending updates. + */ + protected void executePendingUpdate() { + for (ScheduleRow row : mPendingUpdate) { + int index = indexOf(row); + if (index != -1) { + notifyArrayItemRangeChanged(index, 1); + } + } + mPendingUpdate.clear(); } /** * To check whether the recording should be kept or not. */ - protected boolean willBeKept(ScheduledRecording recording) { - return recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + protected boolean willBeKept(ScheduledRecording schedule) { + // CANCELED state means that the schedule was removed temporarily, which should be shown + // in the list so that the user can reschedule it. + return schedule.getEndTimeMs() > System.currentTimeMillis() + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); + } + + /** + * Handle the message to update/remove rows. + */ + protected void handleUpdateRow(long currentTimeMs) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.getEndTimeMs() <= currentTimeMs) { + removeScheduleRow(row); + } + } + } + } + + /** + * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. + */ + protected long getNextTimerMs(long currentTimeMs) { + long earliest = Long.MAX_VALUE; + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + // If the schedule was finished earlier than the end time, it should be removed + // when it reaches the end time in this class. + ScheduleRow row = (ScheduleRow) item; + if (earliest > row.getEndTimeMs()) { + earliest = row.getEndTimeMs(); + } + } + } + return earliest; + } + + /** + * Send update message at the time returned by {@link #getNextTimerMs}. + */ + protected final void sendNextUpdateMessage(long currentTimeMs) { + mHandler.removeMessages(MSG_UPDATE_ROW); + long nextTime = getNextTimerMs(currentTimeMs); + if (nextTime != Long.MAX_VALUE) { + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, + nextTime - System.currentTimeMillis()); + } } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 23aaf4c3..1257e725 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -16,17 +16,20 @@ package com.android.tv.dvr.ui.list; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.media.tv.TvInputInfo; +import android.content.res.Resources; +import android.os.Build; +import android.support.annotation.IntDef; import android.support.v17.leanback.widget.RowPresenter; import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Range; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; @@ -37,140 +40,137 @@ import android.widget.Toast; 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.DvrManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; -import java.util.ArrayList; -import java.util.HashMap; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ +@TargetApi(Build.VERSION_CODES.N) public class ScheduleRowPresenter extends RowPresenter { - private Context mContext; - private Set<ScheduleRowClickListener> mListeners = new ArraySet<>(); - private final Drawable mBeingRecordedDrawable; - - private final Map<String, HashMap<Long, ScheduledRecording>> mInputScheduleMap = new - HashMap<>(); - private final List<ScheduledRecording> mConflicts = new ArrayList<>(); - // TODO: Handle input schedule map and conflicts info in the adapter. - - private final Drawable mOnAirDrawable; - private final Drawable mCancelDrawable; - private final Drawable mScheduleDrawable; + private static final String TAG = "ScheduleRowPresenter"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ACTION_START_RECORDING, ACTION_STOP_RECORDING, ACTION_CREATE_SCHEDULE, + ACTION_REMOVE_SCHEDULE}) + public @interface ScheduleRowAction {} + /** An action to start recording. */ + public static final int ACTION_START_RECORDING = 1; + /** An action to stop recording. */ + public static final int ACTION_STOP_RECORDING = 2; + /** An action to create schedule for the row. */ + public static final int ACTION_CREATE_SCHEDULE = 3; + /** An action to remove the schedule. */ + public static final int ACTION_REMOVE_SCHEDULE = 4; + + private final Context mContext; + private final DvrManager mDvrManager; + private final DvrScheduleManager mDvrScheduleManager; private final String mTunerConflictWillNotBeRecordedInfo; private final String mTunerConflictWillBePartiallyRecordedInfo; - private final String mInfoSeparator; + private final int mAnimationDuration; + + private int mLastFocusedViewId; /** * A ViewHolder for {@link ScheduleRow} */ public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder { + private ScheduleRowPresenter mPresenter; + @ScheduleRowAction private int[] mActions; private boolean mLtr; private LinearLayout mInfoContainer; - private RelativeLayout mScheduleActionContainer; - private RelativeLayout mDeleteActionContainer; + // The first action is on the right of the second action. + private RelativeLayout mSecondActionContainer; + private RelativeLayout mFirstActionContainer; private View mSelectorView; private TextView mTimeView; private TextView mProgramTitleView; private TextView mInfoSeparatorView; private TextView mChannelNameView; private TextView mConflictInfoView; - private ImageView mScheduleActionView; - private ImageView mDeleteActionView; - - private ScheduledRecording mRecording; + private ImageView mSecondActionView; + private ImageView mFirstActionView; + + private Runnable mPendingAnimationRunnable; + + private final int mSelectorTranslationDelta; + private final int mSelectorWidthDelta; + private final int mInfoContainerTargetWidthWithNoAction; + private final int mInfoContainerTargetWidthWithOneAction; + private final int mInfoContainerTargetWidthWithTwoAction; + private final int mRoundRectRadius; + + private final OnFocusChangeListener mOnFocusChangeListener = + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean focused) { + view.post(new Runnable() { + @Override + public void run() { + if (view.isFocused()) { + mPresenter.mLastFocusedViewId = view.getId(); + } + updateSelector(); + } + }); + } + }; - public ScheduleRowViewHolder(View view) { + public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { super(view); + mPresenter = presenter; mLtr = view.getContext().getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container); - mScheduleActionContainer = (RelativeLayout) view.findViewById( - R.id.action_schedule_container); - mScheduleActionView = (ImageView) view.findViewById(R.id.action_schedule); - mDeleteActionContainer = (RelativeLayout) view.findViewById( - R.id.action_delete_container); - mDeleteActionView = (ImageView) view.findViewById(R.id.action_delete); + mSecondActionContainer = (RelativeLayout) view.findViewById( + R.id.action_second_container); + mSecondActionView = (ImageView) view.findViewById(R.id.action_second); + mFirstActionContainer = (RelativeLayout) view.findViewById( + R.id.action_first_container); + mFirstActionView = (ImageView) view.findViewById(R.id.action_first); mSelectorView = view.findViewById(R.id.selector); mTimeView = (TextView) view.findViewById(R.id.time); mProgramTitleView = (TextView) view.findViewById(R.id.program_title); mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator); mChannelNameView = (TextView) view.findViewById(R.id.channel_name); mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info); - - mInfoContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - - mDeleteActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - - mScheduleActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - } - - /** - * Sets scheduled recording. - */ - public void setRecording(ScheduledRecording recording) { - mRecording = recording; - } - - /** - * Returns Info container. - */ - public LinearLayout getInfoContainer() { - return mInfoContainer; - } - - /** - * Returns schedule action container. - */ - public RelativeLayout getScheduleActionContainer() { - return mScheduleActionContainer; - } - - /** - * Returns delete action container. - */ - public RelativeLayout getDeleteActionContainer() { - return mDeleteActionContainer; + Resources res = view.getResources(); + mSelectorTranslationDelta = + res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_translation_delta); + mSelectorWidthDelta = res.getDimensionPixelSize( + R.dimen.dvr_schedules_item_focus_width_delta); + mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius); + int fullWidth = res.getDimensionPixelSize( + R.dimen.dvr_schedules_item_width) + - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding); + mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius; + mInfoContainerTargetWidthWithOneAction = fullWidth + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width) + + mRoundRectRadius + mSelectorWidthDelta; + mInfoContainerTargetWidthWithTwoAction = mInfoContainerTargetWidthWithOneAction + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size); + + mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener); + mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); + mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); } /** @@ -187,92 +187,55 @@ public class ScheduleRowPresenter extends RowPresenter { return mProgramTitleView; } - /** - * Returns subtitle view. - */ - public TextView getChannelNameView() { - return mChannelNameView; - } - - /** - * Returns conflict information view. - */ - public TextView getConflictInfoView() { - return mConflictInfoView; - } - - /** - * Returns schedule action view. - */ - public ImageView getScheduleActionView() { - return mScheduleActionView; - } - - /** - * Returns delete action view. - */ - public ImageView getDeleteActionView() { - return mDeleteActionView; - } - - /** - * Returns scheduled recording. - */ - public ScheduledRecording getRecording() { - return mRecording; - } - private void updateSelector() { - // TODO: Support RTL language int animationDuration = mSelectorView.getResources().getInteger( android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); - int roundRectRadius = view.getResources().getDimensionPixelSize( - R.dimen.dvr_schedules_selector_radius); - if (mInfoContainer.isFocused() || mScheduleActionContainer.isFocused() - || mDeleteActionContainer.isFocused()) { + if (mInfoContainer.isFocused() || mSecondActionContainer.isFocused() + || mFirstActionContainer.isFocused()) { final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams(); final int targetWidth; if (mInfoContainer.isFocused()) { - if (mScheduleActionContainer.getVisibility() == View.GONE - && mDeleteActionContainer.getVisibility() == View.GONE) { - targetWidth = mInfoContainer.getWidth() + 2 * roundRectRadius; + // Use actions to check the visibility of the actions instead of calling + // View.getVisibility() because the view could be on the hiding animation. + if (mActions == null || mActions.length == 0) { + targetWidth = mInfoContainerTargetWidthWithNoAction; + } else if (mActions.length == 1) { + targetWidth = mInfoContainerTargetWidthWithOneAction; } else { - targetWidth = mInfoContainer.getWidth() + roundRectRadius; - } - } else if (mScheduleActionContainer.isFocused()) { - if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) { - targetWidth = mScheduleActionContainer.getWidth(); - } else { - targetWidth = 2 * roundRectRadius; + targetWidth = mInfoContainerTargetWidthWithTwoAction; } + } else if (mSecondActionContainer.isFocused()) { + targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius); } else { - targetWidth = mDeleteActionContainer.getWidth() + roundRectRadius; + targetWidth = mFirstActionContainer.getWidth() + mRoundRectRadius + + mSelectorTranslationDelta; } float targetTranslationX; if (mInfoContainer.isFocused()) { - targetTranslationX = mLtr ? mInfoContainer.getLeft() - roundRectRadius + targetTranslationX = mLtr ? mInfoContainer.getLeft() - mRoundRectRadius - mSelectorView.getLeft() : - mInfoContainer.getRight() + roundRectRadius - mInfoContainer.getRight(); - } else if (mScheduleActionContainer.isFocused()) { - if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) { - targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() - + mInfoContainer.getRight() + mRoundRectRadius - mSelectorView.getRight(); + } else if (mSecondActionContainer.isFocused()) { + if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) { + targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - mSelectorView.getLeft() - : mScheduleActionContainer.getRight() - mSelectorView.getRight(); + : mSecondActionContainer.getRight() - mSelectorView.getRight(); } else { - targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() - - (roundRectRadius - mScheduleActionContainer.getWidth() / 2) - + targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - + (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - mSelectorView.getLeft() - : mScheduleActionContainer.getRight() + - (roundRectRadius - mScheduleActionContainer.getWidth() / 2) - + : mSecondActionContainer.getRight() + + (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - mSelectorView.getRight(); } } else { - targetTranslationX = mLtr ? mDeleteActionContainer.getLeft() - - mSelectorView.getLeft() - : mDeleteActionContainer.getRight() - mSelectorView.getRight(); + targetTranslationX = mLtr ? mFirstActionContainer.getLeft() + - mSelectorTranslationDelta - mSelectorView.getLeft() + : mFirstActionContainer.getRight() + mSelectorTranslationDelta + - mSelectorView.getRight(); } if (mSelectorView.getAlpha() == 0) { @@ -294,10 +257,14 @@ public class ScheduleRowPresenter extends RowPresenter { mSelectorView.requestLayout(); } }).setDuration(animationDuration).setInterpolator(interpolator).start(); + if (mPendingAnimationRunnable != null) { + mPendingAnimationRunnable.run(); + mPendingAnimationRunnable = null; + } } else { mSelectorView.animate().cancel(); mSelectorView.animate().alpha(0f).setDuration(animationDuration) - .setInterpolator(interpolator).start(); + .setInterpolator(interpolator).setUpdateListener(null).start(); } } @@ -338,23 +305,20 @@ public class ScheduleRowPresenter extends RowPresenter { setHeaderPresenter(null); setSelectEffectEnabled(false); mContext = context; - mBeingRecordedDrawable = mContext.getDrawable(R.drawable.ic_record_stop); - mOnAirDrawable = mContext.getDrawable(R.drawable.ic_record_start); - mCancelDrawable = mContext.getDrawable(R.drawable.ic_dvr_cancel); - mScheduleDrawable = mContext.getDrawable(R.drawable.ic_scheduled_recording); + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mTunerConflictWillNotBeRecordedInfo = mContext.getString( R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info); mTunerConflictWillBePartiallyRecordedInfo = mContext.getString( R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded); - mInfoSeparator = mContext.getString(R.string.dvr_schedules_information_separator); - updateInputScheduleMap(); + mAnimationDuration = mContext.getResources().getInteger( + android.R.integer.config_shortAnimTime); } @Override public ViewHolder createRowViewHolder(ViewGroup parent) { - View view = LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item, - parent, false); - return onGetScheduleRowViewHolder(view); + return onGetScheduleRowViewHolder(LayoutInflater.from(mContext) + .inflate(R.layout.dvr_schedules_item, parent, false)); } /** @@ -365,396 +329,467 @@ public class ScheduleRowPresenter extends RowPresenter { } /** - * Returns be recorded drawable which is for being recorded scheduled recordings. - */ - protected Drawable getBeingRecordedDrawable() { - return mBeingRecordedDrawable; - } - - /** - * Returns on air drawable which is for on air but not being recorded scheduled recordings. + * Returns DVR manager. */ - protected Drawable getOnAirDrawable() { - return mOnAirDrawable; - } - - /** - * Returns cancel drawable which is for cancelling scheduled recording. - */ - protected Drawable getCancelDrawable() { - return mCancelDrawable; - } - - /** - * Returns schedule drawable which is for scheduling. - */ - protected Drawable getScheduleDrawable() { - return mScheduleDrawable; - } - - /** - * Returns conflicting scheduled recordings. - */ - protected List<ScheduledRecording> getConflicts() { - return mConflicts; + protected DvrManager getDvrManager() { + return mDvrManager; } @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - ScheduleRow scheduleRow = (ScheduleRow) item; - ScheduledRecording recording = scheduleRow.getRecording(); - // TODO: Do not show separator in the first row. + ScheduleRow row = (ScheduleRow) item; + @ScheduleRowAction int[] actions = getAvailableActions(row); + viewHolder.mActions = actions; viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onInfoClicked(scheduleRow); + onInfoClicked(row); } }); - viewHolder.mDeleteActionContainer.setOnClickListener(new View.OnClickListener() { + viewHolder.mFirstActionContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onDeleteClicked(scheduleRow, viewHolder); + onActionClicked(actions[0], row); } }); - viewHolder.mScheduleActionContainer.setOnClickListener(new View.OnClickListener() { + viewHolder.mSecondActionContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onScheduleClicked(scheduleRow); + onActionClicked(actions[1], row); } }); - viewHolder.mTimeView.setText(onGetRecordingTimeText(recording)); - Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager() - .getChannel(recording.getChannelId()); - String programInfoText = onGetProgramInfoText(recording); + viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); + String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { int durationMins = - Math.max((int) TimeUnit.MILLISECONDS.toMinutes(recording.getDuration()), 1); + Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } - String channelName = channel != null ? channel.getDisplayName() : null; + String channelName = getChannelNameText(row); viewHolder.mProgramTitleView.setText(programInfoText); viewHolder.mInfoSeparatorView.setVisibility((!TextUtils.isEmpty(programInfoText) && !TextUtils.isEmpty(channelName)) ? View.VISIBLE : View.GONE); viewHolder.mChannelNameView.setText(channelName); - if (!scheduleRow.isRemoveScheduleChecked()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable); - } else { - viewHolder.mDeleteActionView.setImageDrawable(mCancelDrawable); + if (actions != null) { + switch (actions.length) { + case 2: + viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1])); + // pass through + case 1: + viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0])); + break; } - } else { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable); + } + if (mDvrManager.isConflicting(row.getSchedule())) { + String conflictInfo; + if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { + conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; } else { - viewHolder.mDeleteActionView.setImageDrawable(mScheduleDrawable); + conflictInfo = mTunerConflictWillNotBeRecordedInfo; } - viewHolder.mProgramTitleView.setTextColor( - mContext.getResources().getColor(R.color.dvr_schedules_item_info, null)); + viewHolder.mConflictInfoView.setText(conflictInfo); + viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); + } else { + viewHolder.mConflictInfoView.setVisibility(View.GONE); + } + if (shouldBeGrayedOut(row)) { + viewHolder.greyOutInfo(); + } else { + viewHolder.whiteBackInfo(); + } + updateActionContainer(viewHolder, viewHolder.isSelected()); + } + + private int getImageForAction(@ScheduleRowAction int action) { + switch (action) { + case ACTION_START_RECORDING: + return R.drawable.ic_record_start; + case ACTION_STOP_RECORDING: + return R.drawable.ic_record_stop; + case ACTION_CREATE_SCHEDULE: + return R.drawable.ic_scheduled_recording; + case ACTION_REMOVE_SCHEDULE: + return R.drawable.ic_dvr_cancel; + default: + return 0; } - viewHolder.mRecording = recording; - onBindRowViewHolderInternal(viewHolder, scheduleRow); } /** * Returns view holder for schedule row. */ protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { - return new ScheduleRowViewHolder(view); + return new ScheduleRowViewHolder(view, this); } /** * Returns time text for time view from scheduled recording. */ - protected String onGetRecordingTimeText(ScheduledRecording recording) { - return Utils.getDurationString(mContext, recording.getStartTimeMs(), - recording.getEndTimeMs(), true, false, true, 0); + protected String onGetRecordingTimeText(ScheduleRow row) { + return Utils.getDurationString(mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, + false, true, 0); } /** * Returns program info text for program title view. */ - protected String onGetProgramInfoText(ScheduledRecording recording) { - if (recording != null) { - return recording.getProgramTitle(); - } - return null; + protected String onGetProgramInfoText(ScheduleRow row) { + return row.getProgramTitleWithEpisodeNumber(mContext); } - /** - * Internal method for onBindRowViewHolder, can be customized by subclass. - */ - protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder, ScheduleRow - scheduleRow) { - if (mConflicts.contains(scheduleRow.getRecording())) { - viewHolder.mScheduleActionView.setImageDrawable(mScheduleDrawable); - String conflictInfo = mTunerConflictWillNotBeRecordedInfo; - // TODO: It's also possible for the NonStarted schedules to be partially recorded. - if (viewHolder.mRecording.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; - } - viewHolder.mConflictInfoView.setText(conflictInfo); - // TODO: Add 12dp warning icon to conflict info. - viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); - viewHolder.greyOutInfo(); - } else { - viewHolder.mScheduleActionContainer.setVisibility(View.GONE); - viewHolder.mConflictInfoView.setVisibility(View.GONE); - if (!scheduleRow.isRemoveScheduleChecked()) { - viewHolder.whiteBackInfo(); - } - } + private String getChannelNameText(ScheduleRow row) { + Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager() + .getChannel(row.getChannelId()); + return channel == null ? null : + TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : + channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); } /** - * Updates input schedule map. + * Called when user click Info in {@link ScheduleRow}. */ - private void updateInputScheduleMap() { - mInputScheduleMap.clear(); - List<ScheduledRecording> allRecordings = TvApplication.getSingletons(getContext()) - .getDvrDataManager().getAvailableScheduledRecordings(); - for(ScheduledRecording recording : allRecordings) { - addScheduledRecordingToMap(recording); + protected void onInfoClicked(ScheduleRow scheduleRow) { + ScheduledRecording schedule = scheduleRow.getSchedule(); + if (schedule != null) { + DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); } - updateConflicts(); } /** - * Updates conflicting scheduled recordings. + * Called when the button in a row is clicked. */ - private void updateConflicts() { - mConflicts.clear(); - for (String inputId : mInputScheduleMap.keySet()) { - TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); - if (input == null) { - continue; - } - mConflicts.addAll(DvrScheduleManager.getConflictingSchedules( - new ArrayList<>(mInputScheduleMap.get(inputId).values()), - input.getTunerCount())); + protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) { + switch (action) { + case ACTION_START_RECORDING: + onStartRecording(row); + break; + case ACTION_STOP_RECORDING: + onStopRecording(row); + break; + case ACTION_CREATE_SCHEDULE: + onCreateSchedule(row); + break; + case ACTION_REMOVE_SCHEDULE: + onRemoveSchedule(row); + break; } } /** - * Adds a scheduled recording to the map, it happens when user undo cancel. + * Action handler for {@link #ACTION_START_RECORDING}. */ - private void addScheduledRecordingToMap(ScheduledRecording recording) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - recording.getChannelId()); - if (input == null) { + protected void onStartRecording(ScheduleRow row) { + ScheduledRecording schedule = row.getSchedule(); + if (schedule == null) { + // This row has been deleted. return; } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - schedulesMap = new HashMap<>(); - mInputScheduleMap.put(inputId, schedulesMap); + // Checks if there are current recordings that will be stopped by schedule this program. + // If so, shows confirmation dialog to users. + List<ScheduledRecording> conflictSchedules = mDvrScheduleManager.getConflictingSchedules( + schedule.getChannelId(), System.currentTimeMillis(), schedule.getEndTimeMs()); + for (int i = conflictSchedules.size() - 1; i >= 0; i--) { + ScheduledRecording conflictSchedule = conflictSchedules.get(i); + if (conflictSchedule.isInProgress()) { + DvrUiHelper.showStopRecordingDialog((Activity) mContext, + conflictSchedule.getChannelId(), + DvrStopRecordingFragment.REASON_ON_CONFLICT, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + onStartRecordingInternal(row); + } + } + }); + return; + } } - schedulesMap.put(recording.getId(), recording); + onStartRecordingInternal(row); } - /** - * Called when a scheduled recording is added into dvr date manager. - */ - public void onScheduledRecordingAdded(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || recording - .getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - addScheduledRecordingToMap(recording); - updateConflicts(); + private void onStartRecordingInternal(ScheduleRow row) { + if (row.isOnAir() && !row.isRecordingInProgress() && !row.isStartRecordingRequested()) { + row.setStartRecordingRequested(true); + if (row.isRecordingNotStarted()) { + mDvrManager.setHighestPriority(row.getSchedule()); + } else if (row.isRecordingFinished()) { + mDvrManager.addSchedule(ScheduledRecording.buildFrom(row.getSchedule()) + .setId(ScheduledRecording.ID_NOT_SET) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state to start recording: " + + row); + return; + } + String msg = mContext.getString(R.string.dvr_msg_current_program_scheduled, + row.getSchedule().getProgramTitle(), + Utils.toTimeString(row.getEndTimeMs(), false)); + ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); } } /** - * Adds a scheduled recording to the map, it happens when user undo cancel. + * Action handler for {@link #ACTION_STOP_RECORDING}. */ - private void updateScheduledRecordingToMap(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || - recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - recording.getChannelId()); - if (input == null) { - return; - } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - addScheduledRecordingToMap(recording); - return; + protected void onStopRecording(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. + return; + } + if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + row.setStopRecordingRequested(true); + mDvrManager.stopRecording(row.getSchedule()); + CharSequence deletedInfo = onGetProgramInfoText(row); + if (TextUtils.isEmpty(deletedInfo)) { + deletedInfo = getChannelNameText(row); } - schedulesMap.put(recording.getId(), recording); - } else { - removeScheduledRecordingFromMap(recording); + ToastUtils.show(mContext, mContext.getResources() + .getString(R.string.dvr_schedules_deletion_info, deletedInfo), + Toast.LENGTH_SHORT); } } /** - * Called when a scheduled recording is updated in dvr date manager. + * Action handler for {@link #ACTION_CREATE_SCHEDULE}. */ - public void onScheduledRecordingUpdated(ScheduledRecording recording) { - updateScheduledRecordingToMap(recording); - updateConflicts(); + protected void onCreateSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. + return; + } + if (!row.isOnAir()) { + if (row.isScheduleCanceled()) { + mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); + String msg = mContext.getString(R.string.dvr_msg_program_scheduled, + row.getSchedule().getProgramTitle()); + ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); + } else if (mDvrManager.isConflicting(row.getSchedule())) { + mDvrManager.setHighestPriority(row.getSchedule()); + } + } } /** - * Removes a scheduled recording from the map, it happens when user cancel schedule. + * Action handler for {@link #ACTION_REMOVE_SCHEDULE}. */ - private void removeScheduledRecordingFromMap(ScheduledRecording recording) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, recording.getChannelId()); - if (input == null) { + protected void onRemoveSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. return; } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - return; + CharSequence deletedInfo = null; + if (row.isOnAir()) { + if (row.isRecordingNotStarted()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.removeScheduledRecording(row.getSchedule()); + } + } else { + if (mDvrManager.isConflicting(row.getSchedule()) + && !shouldKeepScheduleAfterRemoving()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.removeScheduledRecording(row.getSchedule()); + } else if (row.isRecordingNotStarted()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_CANCELED) + .build()); + } } - schedulesMap.remove(recording.getId()); - if (schedulesMap.isEmpty()) { - mInputScheduleMap.remove(inputId); + if (deletedInfo != null) { + ToastUtils.show(mContext, mContext.getResources() + .getString(R.string.dvr_schedules_deletion_info, deletedInfo), + Toast.LENGTH_SHORT); } } - /** - * Called when a scheduled recording is removed from dvr date manager. - */ - public void onScheduledRecordingRemoved(ScheduledRecording recording) { - removeScheduledRecordingFromMap(recording); - updateConflicts(); + private CharSequence getDeletedInfo(ScheduleRow row) { + CharSequence deletedInfo = onGetProgramInfoText(row); + if (TextUtils.isEmpty(deletedInfo)) { + return getChannelNameText(row); + } + return deletedInfo; } - /** - * Called when user click Info in {@link ScheduleRow}. - */ - protected void onInfoClicked(ScheduleRow scheduleRow) { - DvrUiHelper.startDetailsActivity((Activity) mContext, - scheduleRow.getRecording(), null, true); + @Override + protected void onRowViewSelected(ViewHolder vh, boolean selected) { + super.onRowViewSelected(vh, selected); + updateActionContainer(vh, selected); } /** - * Called when user click schedule in {@link ScheduleRow}. + * Internal method for onRowViewSelected, can be customized by subclass. */ - protected void onScheduleClicked(ScheduleRow scheduleRow) { - ScheduledRecording scheduledRecording = scheduleRow.getRecording(); - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - scheduledRecording.getChannelId()); - if (input == null) { - return; - } - List<ScheduledRecording> allScheduledRecordings = new ArrayList<ScheduledRecording>( - mInputScheduleMap.get(input.getId()).values()); - long maxPriority = scheduledRecording.getPriority(); - for (ScheduledRecording recording : allScheduledRecordings) { - if (scheduledRecording.isOverLapping( - new Range<>(recording.getStartTimeMs(), recording.getEndTimeMs()))) { - if (maxPriority < recording.getPriority()) { - maxPriority = recording.getPriority(); + private void updateActionContainer(ViewHolder vh, boolean selected) { + ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; + viewHolder.mSecondActionContainer.animate().setListener(null).cancel(); + viewHolder.mFirstActionContainer.animate().setListener(null).cancel(); + if (selected && viewHolder.mActions != null) { + switch (viewHolder.mActions.length) { + case 2: + prepareShowActionView(viewHolder.mSecondActionContainer); + prepareShowActionView(viewHolder.mFirstActionContainer); + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + showActionView(viewHolder.mSecondActionContainer); + showActionView(viewHolder.mFirstActionContainer); + } + }; + break; + case 1: + prepareShowActionView(viewHolder.mFirstActionContainer); + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + showActionView(viewHolder.mFirstActionContainer); + } + }; + if (mLastFocusedViewId == R.id.action_second_container) { + mLastFocusedViewId = R.id.info_container; + } + break; + case 0: + default: + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + hideActionView(viewHolder.mFirstActionContainer, View.GONE); + } + }; + if (mLastFocusedViewId == R.id.action_first_container + || mLastFocusedViewId == R.id.action_second_container) { + mLastFocusedViewId = R.id.info_container; + } + break; + } + View view = viewHolder.view.findViewById(mLastFocusedViewId); + if (view != null && view.getVisibility() == View.VISIBLE) { + // When the row is selected, information container gets the initial focus. + // To give the focus to the same control as the previous row, we need to call + // requestFocus() explicitly. + if (view.hasFocus()) { + viewHolder.mPendingAnimationRunnable.run(); + } else { + view.requestFocus(); } } + } else { + viewHolder.mPendingAnimationRunnable = null; + hideActionView(viewHolder.mFirstActionContainer, View.GONE); + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + } + } + + private void prepareShowActionView(View view) { + if (view.getVisibility() != View.VISIBLE) { + view.setAlpha(0.0f); } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording) - .setPriority(maxPriority + 1).build()); - updateConflicts(); + view.setVisibility(View.VISIBLE); } /** - * Called when user click delete in {@link ScheduleRow}. + * Add animation when view is visible. */ - protected void onDeleteClicked(ScheduleRow scheduleRow, ViewHolder vh) { - ScheduledRecording recording = scheduleRow.getRecording(); - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - if (!scheduleRow.isRemoveScheduleChecked()) { - if (mConflicts.contains(recording)) { - TvApplication.getSingletons(mContext) - .getDvrManager().removeScheduledRecording(recording); - } + private void showActionView(View view) { + view.animate().alpha(1.0f).setInterpolator(new DecelerateInterpolator()) + .setDuration(mAnimationDuration).start(); + } - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable); - // TODO: Replace an icon whose size is the same as scheudle. - } else { - viewHolder.getDeleteActionView().setImageDrawable(mScheduleDrawable); - } - viewHolder.greyOutInfo(); - scheduleRow.setRemoveScheduleChecked(true); - CharSequence deletedInfo = viewHolder.getProgramTitleView().getText(); - if (TextUtils.isEmpty(deletedInfo)) { - deletedInfo = viewHolder.getChannelNameView().getText(); - } - Toast.makeText(mContext, mContext.getResources() - .getString(R.string.dvr_schedules_deletion_info, deletedInfo), - Toast.LENGTH_SHORT).show(); - removeScheduledRecordingFromMap(recording); - } else { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable); - // TODO: Replace an icon whose size is the same as scheudle. - } else { - viewHolder.getDeleteActionView().setImageDrawable(mCancelDrawable); + /** + * Add animation when view change to invisible. + */ + private void hideActionView(View view, int visibility) { + if (view.getVisibility() != View.VISIBLE) { + if (view.getVisibility() != visibility) { + view.setVisibility(visibility); } - viewHolder.whiteBackInfo(); - scheduleRow.setRemoveScheduleChecked(false); - addScheduledRecordingToMap(recording); - } - updateConflicts(); - for (ScheduleRowClickListener l : mListeners) { - l.onDeleteClicked(scheduleRow); + return; } + view.animate().alpha(0.0f).setInterpolator(new DecelerateInterpolator()) + .setDuration(mAnimationDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(visibility); + view.animate().setListener(null); + } + }).start(); } /** - * Adds {@link ScheduleRowClickListener}. + * Returns the available actions according to the row's state. It should be the reverse order + * with that in the screen. */ - public void addListener(ScheduleRowClickListener scheduleRowClickListener) { - mListeners.add(scheduleRowClickListener); + @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 (canResolveConflict()) { + // The "START" action can change the conflict states. + return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; + } else { + return new int[] {ACTION_REMOVE_SCHEDULE}; + } + } else if (row.isRecordingFinished()) { + return new int[] {ACTION_START_RECORDING}; + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" + + " available actions(on air): " + row); + } + } else { + if (row.isScheduleCanceled()) { + return new int[] {ACTION_CREATE_SCHEDULE}; + } else if (mDvrManager.isConflicting(row.getSchedule()) && canResolveConflict()) { + return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE}; + } else if (row.isRecordingNotStarted()) { + return new int[] {ACTION_REMOVE_SCHEDULE}; + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" + + " available actions(future schedule): " + row); + } + } + } + return null; } /** - * Removes {@link ScheduleRowClickListener}. + * Check if the conflict can be resolved in this screen. */ - public void removeListener(ScheduleRowClickListener - scheduleRowClickListener) { - mListeners.remove(scheduleRowClickListener); - } - - @Override - protected void onRowViewSelected(ViewHolder vh, boolean selected) { - super.onRowViewSelected(vh, selected); - onRowViewSelectedInternal(vh, selected); + protected boolean canResolveConflict() { + return true; } /** - * Internal method for onRowViewSelected, can be customized by subclass. + * Check if the schedule should be kept after removing it. */ - protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) { - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - boolean isRecordingConflicting = mConflicts.contains(viewHolder.mRecording); - if (selected) { - viewHolder.mDeleteActionContainer.setVisibility(View.VISIBLE); - if (isRecordingConflicting) { - viewHolder.mScheduleActionContainer.setVisibility(View.VISIBLE); - } - } else { - viewHolder.mDeleteActionContainer.setVisibility(View.GONE); - if (isRecordingConflicting) { - viewHolder.mScheduleActionContainer.setVisibility(View.GONE); - } - } + protected boolean shouldKeepScheduleAfterRemoving() { + return false; } /** - * A listener for clicking {@link ScheduleRow}. + * Checks if the row should be grayed out. */ - public interface ScheduleRowClickListener{ - /** - * To notify other observers that delete button has been clicked. - */ - void onDeleteClicked(ScheduleRow scheduleRow); + protected boolean shouldBeGrayedOut(ScheduleRow row) { + return row.getSchedule() == null + || (row.isOnAir() && !row.isRecordingInProgress()) + || mDvrManager.isConflicting(row.getSchedule()) + || row.isScheduleCanceled(); } } diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index d103a533..0fb0924d 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,8 +16,6 @@ package com.android.tv.dvr.ui.list; -import android.support.annotation.Nullable; - import com.android.tv.dvr.SeriesRecording; /** @@ -88,13 +86,6 @@ public abstract class SchedulesHeaderRow { } /** - * Sets the latest time of the list which belongs to the header row. - */ - public void setDeadLineMs(long deadLineMs) { - mDeadLineMs = deadLineMs; - } - - /** * Returns the latest time of the list which belongs to the header row. */ public long getDeadLineMs() { @@ -106,35 +97,26 @@ public abstract class SchedulesHeaderRow { * The header row which represent the series recording. */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { - private SeriesRecording mSeries; - private boolean mCancelAllChecked; + private SeriesRecording mSeriesRecording; public SeriesRecordingHeaderRow(String title, String description, int itemCount, SeriesRecording series) { super(title, description, itemCount); - mSeries = series; - mCancelAllChecked = series.getState() == SeriesRecording.STATE_SERIES_CANCELED; - } - - /** - * Sets cancel all checked status. - */ - public void setCancelAllChecked(boolean checked) { - mCancelAllChecked = checked; + mSeriesRecording = series; } /** - * Returns cancel all checked status. + * Returns the series recording, it is for series schedules list. */ - public boolean isCancelAllChecked() { - return mCancelAllChecked; + public SeriesRecording getSeriesRecording() { + return mSeriesRecording; } /** - * Returns the series recording, it is for series schedules list. + * Sets the series recording. */ - public SeriesRecording getSeriesRecording() { - return mSeries; + public void setSeriesRecording(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; } } } diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index 483962e7..69c33a96 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -20,7 +20,6 @@ import android.animation.ValueAnimator; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v17.leanback.widget.RowPresenter; -import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -36,14 +35,11 @@ import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.ui.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; -import java.util.Set; - /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; - private Set<SchedulesHeaderRowListener> mListeners = new ArraySet<>(); public SchedulesHeaderRowPresenter(Context context) { setHeaderPresenter(null); @@ -59,26 +55,6 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * Adds {@link SchedulesHeaderRowListener}. - */ - public void addListener(SchedulesHeaderRowListener listener) { - mListeners.add(listener); - } - - /** - * Removes {@link SchedulesHeaderRowListener}. - */ - public void removeListener(SchedulesHeaderRowListener listener) { - mListeners.remove(listener); - } - - void notifyUpdateAllScheduleRows() { - for (SchedulesHeaderRowListener listener : mListeners) { - listener.onUpdateAllScheduleRows(); - } - } - - /** * A ViewHolder for {@link SchedulesHeaderRow}. */ public static class SchedulesHeaderRowViewHolder extends RowPresenter.ViewHolder { @@ -147,54 +123,59 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { mCancelDrawable = context.getDrawable(R.drawable.ic_dvr_cancel_large); mResumeDrawable = context.getDrawable(R.drawable.ic_record_start); mSettingsInfo = context.getString(R.string.dvr_series_schedules_settings); - mCancelAllInfo = context.getString(R.string.dvr_series_schedules_cancel_all); - mResumeInfo = context.getString(R.string.dvr_series_schedules_resume); + mCancelAllInfo = context.getString(R.string.dvr_series_schedules_stop); + mResumeInfo = context.getString(R.string.dvr_series_schedules_start); } @Override protected ViewHolder createRowViewHolder(ViewGroup parent) { - return new SeriesRecordingRowViewHolder(getContext(), parent); + return new SeriesHeaderRowViewHolder(getContext(), parent); } @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) { super.onBindRowViewHolder(viewHolder, item); - SeriesRecordingRowViewHolder headerViewHolder = - (SeriesRecordingRowViewHolder) viewHolder; + SeriesHeaderRowViewHolder headerViewHolder = + (SeriesHeaderRowViewHolder) viewHolder; SeriesRecordingHeaderRow header = (SeriesRecordingHeaderRow) item; headerViewHolder.mSeriesSettingsButton.setVisibility( - isSeriesScheduleCanceled(getContext(), header) ? View.INVISIBLE : View.VISIBLE); + header.getSeriesRecording().isStopped() ? View.INVISIBLE : View.VISIBLE); headerViewHolder.mSeriesSettingsButton.setText(mSettingsInfo); setTextDrawable(headerViewHolder.mSeriesSettingsButton, mSettingsDrawable); - if (header.isCancelAllChecked()) { - headerViewHolder.mTogglePauseButton.setText(mResumeInfo); - setTextDrawable(headerViewHolder.mTogglePauseButton, mResumeDrawable); + if (header.getSeriesRecording().isStopped()) { + headerViewHolder.mToggleStartStopButton.setText(mResumeInfo); + setTextDrawable(headerViewHolder.mToggleStartStopButton, mResumeDrawable); } else { - headerViewHolder.mTogglePauseButton.setText(mCancelAllInfo); - setTextDrawable(headerViewHolder.mTogglePauseButton, mCancelDrawable); + headerViewHolder.mToggleStartStopButton.setText(mCancelAllInfo); + setTextDrawable(headerViewHolder.mToggleStartStopButton, mCancelDrawable); } headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { + // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId()); + header.getSeriesRecording().getId(), null, false, false, false); } }); - headerViewHolder.mTogglePauseButton.setOnClickListener(new OnClickListener() { + headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { - if (!header.isCancelAllChecked()) { - DvrUiHelper.showCancelAllSeriesRecordingDialog((DvrSchedulesActivity) view - .getContext()); + if (header.getSeriesRecording().isStopped()) { + // Reset priority to the highest. + SeriesRecording seriesRecording = SeriesRecording + .buildFrom(header.getSeriesRecording()) + .setPriority(TvApplication.getSingletons(getContext()) + .getDvrScheduleManager().suggestNewSeriesPriority()) + .build(); + TvApplication.getSingletons(getContext()).getDvrManager() + .updateSeriesRecording(seriesRecording); + // TODO: pass channel list for settings. + DvrUiHelper.startSeriesSettingsActivity(getContext(), + header.getSeriesRecording().getId(), null, false, false, false); } else { - if (isSeriesScheduleCanceled(getContext(), header)) { - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom(header - .getSeriesRecording()).setState(SeriesRecording - .STATE_SERIES_NORMAL).build()); - } - header.setCancelAllChecked(false); - notifyUpdateAllScheduleRows(); + DvrUiHelper.showCancelAllSeriesRecordingDialog( + (DvrSchedulesActivity) view.getContext(), + header.getSeriesRecording()); } } }); @@ -208,97 +189,85 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } } - private static boolean isSeriesScheduleCanceled(Context context, - SeriesRecordingHeaderRow header) { - return TvApplication.getSingletons(context).getDvrDataManager() - .getSeriesRecording(header.getSeriesRecording().getId()).getState() - == SeriesRecording.STATE_SERIES_CANCELED; - } - /** * A ViewHolder for {@link SeriesRecordingHeaderRow}. */ - public static class SeriesRecordingRowViewHolder extends SchedulesHeaderRowViewHolder { + public static class SeriesHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { private final TextView mSeriesSettingsButton; - private final TextView mTogglePauseButton; + private final TextView mToggleStartStopButton; private final boolean mLtr; private final View mSelector; private View mLastFocusedView; - public SeriesRecordingRowViewHolder(Context context, ViewGroup parent) { + public SeriesHeaderRowViewHolder(Context context, ViewGroup parent) { super(context, parent); mLtr = context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; view.findViewById(R.id.button_container).setVisibility(View.VISIBLE); mSeriesSettingsButton = (TextView) view.findViewById(R.id.series_settings); - mTogglePauseButton = (TextView) view.findViewById(R.id.series_toggle_pause); + mToggleStartStopButton = + (TextView) view.findViewById(R.id.series_toggle_start_stop); mSelector = view.findViewById(R.id.selector); OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean focused) { - onIconFouseChange(view); + view.post(new Runnable() { + @Override + public void run() { + updateSelector(view); + } + }); } }; mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener); - mTogglePauseButton.setOnFocusChangeListener(onFocusChangeListener); - } - - void onIconFouseChange(View focusedView) { - updateSelector(focusedView, mSelector); + mToggleStartStopButton.setOnFocusChangeListener(onFocusChangeListener); } - private void updateSelector(View focusedView, final View selectorView) { - int animationDuration = selectorView.getContext().getResources() + private void updateSelector(View focusedView) { + int animationDuration = mSelector.getContext().getResources() .getInteger(android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); if (focusedView.hasFocus()) { - final ViewGroup.LayoutParams lp = selectorView.getLayoutParams(); + ViewGroup.LayoutParams lp = mSelector.getLayoutParams(); final int targetWidth = focusedView.getWidth(); float targetTranslationX; if (mLtr) { - targetTranslationX = focusedView.getLeft() - selectorView.getLeft(); + targetTranslationX = focusedView.getLeft() - mSelector.getLeft(); } else { - targetTranslationX = focusedView.getRight() - selectorView.getRight(); + targetTranslationX = focusedView.getRight() - mSelector.getRight(); } // if the selector is invisible, set the width and translation X directly - // don't animate. - if (selectorView.getAlpha() == 0) { - selectorView.setTranslationX(targetTranslationX); + if (mSelector.getAlpha() == 0) { + mSelector.setTranslationX(targetTranslationX); lp.width = targetWidth; - selectorView.requestLayout(); + mSelector.requestLayout(); } // animate the selector in and to the proper width and translation X. final float deltaWidth = lp.width - targetWidth; - selectorView.animate().cancel(); - selectorView.animate().translationX(targetTranslationX).alpha(1f) + mSelector.animate().cancel(); + mSelector.animate().translationX(targetTranslationX).alpha(1f) .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // Set width to the proper width for this animation step. lp.width = targetWidth + Math.round( deltaWidth * (1f - animation.getAnimatedFraction())); - selectorView.requestLayout(); + mSelector.requestLayout(); } }).setDuration(animationDuration).setInterpolator(interpolator).start(); mLastFocusedView = focusedView; } else if (mLastFocusedView == focusedView) { - selectorView.animate().cancel(); - selectorView.animate().alpha(0f).setDuration(animationDuration) + mSelector.animate().setUpdateListener(null).cancel(); + mSelector.animate().alpha(0f).setDuration(animationDuration) .setInterpolator(interpolator).start(); mLastFocusedView = null; } } } } - - public interface SchedulesHeaderRowListener { - /** - * Updates all schedule rows. - */ - void onUpdateAllScheduleRows(); - } } diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 8b162c54..3b493774 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -16,12 +16,20 @@ package com.android.tv.dvr.ui.list; +import android.annotation.TargetApi; import android.content.Context; +import android.media.tv.TvInputInfo; +import android.os.Build; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.util.ArrayMap; +import android.util.Log; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; 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.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.SeriesRecording; @@ -30,127 +38,232 @@ import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Map; /** * An adapter for series schedule row. */ +@TargetApi(Build.VERSION_CODES.N) public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { - private static final String TAG = "SeriesScheduleRowAdapter"; + private static final String TAG = "SeriesRowAdapter"; + private static final boolean DEBUG = false; - private SeriesRecording mSeriesRecording; + private final SeriesRecording mSeriesRecording; + private final String mInputId; + private final DvrManager mDvrManager; + private final DvrDataManager mDataManager; + private final Map<Long, Program> mPrograms = new ArrayMap<>(); + private SeriesRecordingHeaderRow mHeaderRow; - public SeriesScheduleRowAdapter(Context context, - ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording) { + public SeriesScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector, + SeriesRecording seriesRecording) { super(context, classPresenterSelector); mSeriesRecording = seriesRecording; + TvInputInfo input = Utils.getTvInputInfoForInputId(context, mSeriesRecording.getInputId()); + if (SoftPreconditions.checkNotNull(input) != null) { + mInputId = input.getId(); + } else { + mInputId = null; + } + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrManager = singletons.getDvrManager(); + mDataManager = singletons.getDvrDataManager(); + setHasStableIds(true); } @Override public void start() { - List<ScheduledRecording> recordings = TvApplication.getSingletons(getContext()) - .getDvrDataManager().getAvailableAndCanceledScheduledRecordings(); - List<ScheduledRecording> seriesScheduledRecordings = new ArrayList<>(); - if (mSeriesRecording == null) { - return; + setPrograms(Collections.emptyList()); + } + + @Override + public void stop() { + super.stop(); + } + + /** + * Sets the programs to show. + */ + public void setPrograms(List<Program> programs) { + if (programs == null) { + programs = Collections.emptyList(); } - for (ScheduledRecording recording : recordings) { - if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { - seriesScheduledRecordings.add(recording); + clear(); + mPrograms.clear(); + List<Program> sortedPrograms = new ArrayList<>(programs); + Collections.sort(sortedPrograms); + List<EpisodicProgramRow> rows = new ArrayList<>(); + mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), + null, sortedPrograms.size(), mSeriesRecording); + for (Program program : sortedPrograms) { + ScheduledRecording schedule = + mDataManager.getScheduledRecordingForProgramId(program.getId()); + if (schedule != null && !willBeKept(schedule)) { + schedule = null; } + rows.add(new EpisodicProgramRow(mInputId, program, schedule, mHeaderRow)); + mPrograms.put(program.getId(), program); } - Collections.sort(seriesScheduledRecordings, - ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); - int dayCountToLastRecording = 0; - if (!seriesScheduledRecordings.isEmpty()) { - long lastRecordingStartTimeMs = seriesScheduledRecordings - .get(seriesScheduledRecordings.size() - 1).getStartTimeMs(); - dayCountToLastRecording = Utils.computeDateDifference(System.currentTimeMillis(), - lastRecordingStartTimeMs) + 1; + mHeaderRow.setDescription(getDescription()); + add(mHeaderRow); + for (EpisodicProgramRow row : rows) { + add(row); } - SchedulesHeaderRow headerRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - getContext().getResources().getQuantityString( - R.plurals.dvr_series_schedules_header_description, dayCountToLastRecording, - dayCountToLastRecording), seriesScheduledRecordings.size(), mSeriesRecording); - add(headerRow); - for (ScheduledRecording recording : seriesScheduledRecordings) { - add(new ScheduleRow(recording, headerRow)); + sendNextUpdateMessage(System.currentTimeMillis()); + } + + private String getDescription() { + int conflicts = 0; + for (long programId : mPrograms.keySet()) { + if (mDvrManager.isConflicting( + mDataManager.getScheduledRecordingForProgramId(programId))) { + ++conflicts; + } } + return conflicts == 0 ? null : getContext().getResources().getQuantityString( + R.plurals.dvr_series_schedules_header_description, conflicts, conflicts); } @Override - public void stop() { - SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG, - "First row is not SchedulesHeaderRow"); - boolean cancelAll = size() > 0 && ((SeriesRecordingHeaderRow) get(0)).isCancelAllChecked(); - if (!cancelAll) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - for (int i = 0; i < size(); i++) { - if (get(i) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(i); - if (scheduleRow.isRemoveScheduleChecked()) { - dvrManager.removeScheduledRecording(scheduleRow.getRecording()); - } - } + public long getId(int position) { + Object obj = get(position); + if (obj instanceof EpisodicProgramRow) { + return ((EpisodicProgramRow) obj).getProgram().getId(); + } + if (obj instanceof SeriesRecordingHeaderRow) { + return 0; + } + return super.getId(position); + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + if (!row.isStartRecordingRequested()) { + row.setSchedule(schedule); + notifyArrayItemRangeChanged(index, 1); } } } @Override - protected void addScheduleRow(ScheduledRecording recording) { - if (recording != null && recording.getSeriesRecordingId() == mSeriesRecording.getId()) { - int index = 0; - for (; index < size(); index++) { - if (get(index) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(index); - if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare( - scheduleRow.getRecording(), recording) > 0) { - break; + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + row.setSchedule(null); + notifyArrayItemRangeChanged(index, 1); + } + } + + @Override + public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + if (conflictChange && isStartOrStopRequested()) { + // Delay the conflict update until it gets the response of the start/stop request. + // The purpose is to avoid the intermediate conflict change. + addPendingUpdate(row); + return; + } + if (row.isStopRecordingRequested()) { + // Wait until the recording is finished + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + row.setStopRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); } + row.setSchedule(null); } + } else if (row.isStartRecordingRequested()) { + // When the start recording was requested, we give the highest priority. So it is + // guaranteed that the state will be changed from NOT_STARTED to the other state. + // Update the row with the next state not to show the intermediate state to avoid + // blinking. + if (schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + row.setStartRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + } + } else if (willBeKept(schedule)) { + row.setSchedule(schedule); + } else { + row.setSchedule(null); } - SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG, - "First row is not SchedulesHeaderRow"); - if (index == 0) { - index++; - } - SchedulesHeaderRow headerRow = (SchedulesHeaderRow) get(0); - headerRow.setItemCount(headerRow.getItemCount() + 1); - ScheduleRow addedRow = new ScheduleRow(recording, headerRow); - add(index, addedRow); - updateHeaderRowDescription(headerRow); + notifyArrayItemRangeChanged(index, 1); } } - @Override - protected void removeScheduleRow(ScheduleRow scheduleRow) { - if (scheduleRow != null) { - remove(scheduleRow); - SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); - // Changes the count information of header which the removed row belongs to. - if (headerRow != null) { - headerRow.setItemCount(headerRow.getItemCount() - 1); - if (headerRow.getItemCount() == 0) { - // TODO: Add a emtpy view. - } else if (get(size() - 1) instanceof ScheduleRow) { - updateHeaderRowDescription(headerRow); + public void onSeriesRecordingUpdated(SeriesRecording seriesRecording) { + if (seriesRecording.getId() == mSeriesRecording.getId()) { + mHeaderRow.setSeriesRecording(seriesRecording); + notifyArrayItemRangeChanged(0, 1); + } + } + + private int findRowIndexByProgramId(long programId) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof EpisodicProgramRow) { + if (((EpisodicProgramRow) item).getProgram().getId() == programId) { + return i; } } } + return -1; } @Override - protected boolean willBeKept(ScheduledRecording recording) { - return super.willBeKept(recording) - || recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED; + public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { + mHeaderRow.setDescription(getDescription()); + super.notifyArrayItemRangeChanged(0, 1); + super.notifyArrayItemRangeChanged(positionStart, itemCount); } - private void updateHeaderRowDescription(SchedulesHeaderRow headerRow) { - int nextDays = Utils.computeDateDifference(System.currentTimeMillis(), - ((ScheduleRow) get(size() - 1)).getRecording().getStartTimeMs()) + 1; - headerRow.setDescription(getContext().getResources() - .getQuantityString(R.plurals.dvr_series_schedules_header_description, - nextDays, nextDays)); - replace(indexOf(headerRow), headerRow); + @Override + protected void handleUpdateRow(long currentTimeMs) { + for (Iterator<Program> iter = mPrograms.values().iterator(); iter.hasNext(); ) { + Program program = iter.next(); + if (program.getEndTimeUtcMillis() <= currentTimeMs) { + // Remove the old program. + removeItems(findRowIndexByProgramId(program.getId()), 1); + iter.remove(); + } else if (program.getStartTimeUtcMillis() < currentTimeMs) { + // Change the button "START RECORDING" + notifyItemRangeChanged(findRowIndexByProgramId(program.getId()), 1); + } + } + } + + /** + * Should take the current time argument which is the time when the programs are checked in + * handler. + */ + @Override + protected long getNextTimerMs(long currentTimeMs) { + long earliest = Long.MAX_VALUE; + for (Program program : mPrograms.values()) { + if (earliest > program.getStartTimeUtcMillis() + && program.getStartTimeUtcMillis() >= currentTimeMs) { + // Need the button from "CREATE SCHEDULE" to "START RECORDING" + earliest = program.getStartTimeUtcMillis(); + } else if (earliest > program.getEndTimeUtcMillis()) { + // Need to remove the row. + earliest = program.getEndTimeUtcMillis(); + } + } + return earliest; } } diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index 4f31528c..5d88579a 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -21,16 +21,16 @@ import android.view.View; 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.ScheduledRecording; -import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { - private boolean mIsCancelAll; + private static final String TAG = "SeriesRowPresenter"; + private boolean mLtr; public SeriesScheduleRowPresenter(Context context) { @@ -40,8 +40,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { } public static class SeriesScheduleRowViewHolder extends ScheduleRowViewHolder { - public SeriesScheduleRowViewHolder(View view) { - super(view); + public SeriesScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { + super(view, presenter); ViewGroup.LayoutParams lp = getTimeView().getLayoutParams(); lp.width = view.getResources().getDimensionPixelSize( R.dimen.dvr_series_schedules_item_time_width); @@ -51,37 +51,29 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { - return new SeriesScheduleRowViewHolder(view); + return new SeriesScheduleRowViewHolder(view, this); } @Override - protected String onGetRecordingTimeText(ScheduledRecording recording) { - return Utils.getDurationString(getContext(), - recording.getStartTimeMs(), recording.getEndTimeMs(), false, true, true, 0); + protected String onGetRecordingTimeText(ScheduleRow row) { + return Utils.getDurationString(getContext(), row.getStartTimeMs(), row.getEndTimeMs(), + false, true, true, 0); } @Override - protected String onGetProgramInfoText(ScheduledRecording recording) { - if (recording != null) { - return recording.getEpisodeDisplayTitle(getContext()); - } - return null; + protected String onGetProgramInfoText(ScheduleRow row) { + return row.getEpisodeDisplayTitle(getContext()); } @Override - protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder, - ScheduleRow scheduleRow) { - mIsCancelAll = ((SeriesRecordingHeaderRow) scheduleRow.getHeaderRow()).isCancelAllChecked(); - boolean isConflicting = getConflicts().contains(scheduleRow.getRecording()); - if (mIsCancelAll || isConflicting || scheduleRow.isRemoveScheduleChecked()) { - viewHolder.greyOutInfo(); - } else { - viewHolder.whiteBackInfo(); - } - if (!mIsCancelAll && isConflicting) { + protected void onBindRowViewHolder(ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + SeriesScheduleRowViewHolder viewHolder = (SeriesScheduleRowViewHolder) vh; + EpisodicProgramRow row = (EpisodicProgramRow) item; + if (getDvrManager().isConflicting(row.getSchedule())) { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( - R.dimen.dvr_schedules_warning_icon_padding)); + R.dimen.dvr_schedules_warning_icon_padding)); if (mLtr) { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_warning_gray600_36dp, 0, 0, 0); @@ -92,26 +84,60 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } - if (mIsCancelAll) { - viewHolder.getInfoContainer().setClickable(false); - viewHolder.getDeleteActionContainer().setVisibility(View.GONE); + } + + @Override + protected void onInfoClicked(ScheduleRow row) { + if (row.getSchedule() != null) { + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); } } @Override - protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) { - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - if (!mIsCancelAll) { - if (selected) { - viewHolder.getDeleteActionContainer().setVisibility(View.VISIBLE); + protected void onStartRecording(ScheduleRow row) { + SoftPreconditions.checkState(row.getSchedule() == null, TAG, + "Start request with the existing schedule: " + row); + row.setStartRecordingRequested(true); + getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram()); + } + + @Override + protected void onStopRecording(ScheduleRow row) { + SoftPreconditions.checkState(row.getSchedule() != null, TAG, + "Stop request with the null schedule: " + row); + row.setStopRecordingRequested(true); + getDvrManager().stopRecording(row.getSchedule()); + } + + @Override + protected void onCreateSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram()); + } else { + super.onCreateSchedule(row); + } + } + + @Override + @ScheduleRowAction + protected int[] getAvailableActions(ScheduleRow row) { + if (row.getSchedule() == null) { + if (row.isOnAir()) { + return new int[] {ACTION_START_RECORDING}; } else { - viewHolder.getDeleteActionContainer().setVisibility(View.GONE); + return new int[] {ACTION_CREATE_SCHEDULE}; } } + return super.getAvailableActions(row); + } + + @Override + protected boolean canResolveConflict() { + return false; } @Override - protected void onInfoClicked(ScheduleRow scheduleRow) { - DvrUiHelper.startSchedulesActivity(getContext(), scheduleRow.getRecording()); + protected boolean shouldKeepScheduleAfterRemoving() { + return true; } } |