aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2016-10-26 14:03:09 -0700
committerNick Chalko <nchalko@google.com>2016-10-31 10:36:49 -0700
commitd41f0075a7d2ea826204e81fcec57d0aa57171a9 (patch)
treecb30cfbafd80e01d314868cdc36e783d39981119 /src/com/android/tv/dvr
parent5e0ec06a797e3497da94390c63c7072de442695b (diff)
downloadTV-d41f0075a7d2ea826204e81fcec57d0aa57171a9.tar.gz
Sync to ub-tv-killing at 6f6e46557accb62c9548e4177d6005aa944dbf33
Change-Id: I873644d6d9d0110c981ef6075cb4019c16bbb94b
Diffstat (limited to 'src/com/android/tv/dvr')
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java55
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java11
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java408
-rw-r--r--src/com/android/tv/dvr/DvrDbSync.java208
-rw-r--r--src/com/android/tv/dvr/DvrManager.java465
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackActivity.java2
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java44
-rw-r--r--src/com/android/tv/dvr/DvrPlayer.java84
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java3
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java439
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java376
-rw-r--r--src/com/android/tv/dvr/DvrUiHelper.java154
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java43
-rw-r--r--src/com/android/tv/dvr/EpisodicProgramLoadTask.java382
-rw-r--r--src/com/android/tv/dvr/InputTaskScheduler.java85
-rw-r--r--src/com/android/tv/dvr/RecordedProgram.java43
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java78
-rw-r--r--src/com/android/tv/dvr/ScheduledRecording.java48
-rw-r--r--src/com/android/tv/dvr/Scheduler.java22
-rw-r--r--src/com/android/tv/dvr/SeriesRecording.java48
-rw-r--r--src/com/android/tv/dvr/SeriesRecordingScheduler.java412
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java15
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java6
-rw-r--r--src/com/android/tv/dvr/ui/DetailsContentPresenter.java284
-rw-r--r--src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrBrowseFragment.java157
-rw-r--r--src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java65
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsActivity.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsFragment.java109
-rw-r--r--src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java11
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java88
-rw-r--r--src/com/android/tv/dvr/ui/DvrItemPresenter.java80
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java20
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java12
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java34
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java43
-rw-r--r--src/com/android/tv/dvr/ui/DvrSchedulesActivity.java86
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java1
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java154
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java40
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java57
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java (renamed from src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java)6
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java104
-rw-r--r--src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java37
-rw-r--r--src/com/android/tv/dvr/ui/PrioritySettingsFragment.java7
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java110
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramPresenter.java30
-rw-r--r--src/com/android/tv/dvr/ui/RecordingCardView.java13
-rw-r--r--src/com/android/tv/dvr/ui/RecordingDetailsFragment.java13
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java25
-rw-r--r--src/com/android/tv/dvr/ui/SeriesDeletionFragment.java13
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java162
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java82
-rw-r--r--src/com/android/tv/dvr/ui/SeriesSettingsFragment.java221
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java127
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java35
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java168
-rw-r--r--src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java89
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java176
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java269
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java909
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java34
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java139
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java275
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java98
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;
}
}