aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2016-08-31 16:00:31 -0700
committerNick Chalko <nchalko@google.com>2016-09-07 05:38:33 -0700
commit65fda1eaa94968bb55d5ded10dcb0b3f37fb05f2 (patch)
treeffc8e4c5a71c130d3782bf03e674f9d77ca77f72 /src/com/android/tv/dvr
parentad819718f80e796cf039f96537b5c8cd127c042b (diff)
downloadTV-65fda1eaa94968bb55d5ded10dcb0b3f37fb05f2.tar.gz
Sync to ub-tv-dev at http://ag/1415258
Bug: 30970843 Change-Id: I0aa43094d103de28956a3d9b56a594ea46a20543
Diffstat (limited to 'src/com/android/tv/dvr')
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java185
-rw-r--r--src/com/android/tv/dvr/ConflictChecker.java277
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java153
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java578
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java215
-rw-r--r--src/com/android/tv/dvr/DvrDbSync.java207
-rw-r--r--src/com/android/tv/dvr/DvrManager.java629
-rw-r--r--src/com/android/tv/dvr/DvrPlayActivity.java47
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackActivity.java65
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java297
-rw-r--r--src/com/android/tv/dvr/DvrPlayer.java393
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java34
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java717
-rw-r--r--src/com/android/tv/dvr/DvrSessionManager.java130
-rw-r--r--src/com/android/tv/dvr/DvrStartRecordingReceiver.java3
-rw-r--r--src/com/android/tv/dvr/DvrUiHelper.java382
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java119
-rw-r--r--src/com/android/tv/dvr/IdGenerator.java50
-rw-r--r--src/com/android/tv/dvr/InputTaskScheduler.java378
-rw-r--r--src/com/android/tv/dvr/RecordedProgram.java825
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java302
-rw-r--r--src/com/android/tv/dvr/ScheduledProgramReaper.java20
-rw-r--r--src/com/android/tv/dvr/ScheduledRecording.java660
-rw-r--r--src/com/android/tv/dvr/Scheduler.java258
-rw-r--r--src/com/android/tv/dvr/SeasonRecording.java35
-rw-r--r--src/com/android/tv/dvr/SeriesInfo.java76
-rw-r--r--src/com/android/tv/dvr/SeriesRecording.java749
-rw-r--r--src/com/android/tv/dvr/SeriesRecordingScheduler.java705
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java34
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java137
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java302
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java362
-rw-r--r--src/com/android/tv/dvr/ui/ActionPresenterSelector.java138
-rw-r--r--src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java59
-rw-r--r--src/com/android/tv/dvr/ui/DetailsContent.java207
-rw-r--r--src/com/android/tv/dvr/ui/DetailsContentPresenter.java42
-rw-r--r--src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java88
-rw-r--r--src/com/android/tv/dvr/ui/DvrActivity.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java103
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java107
-rw-r--r--src/com/android/tv/dvr/ui/DvrBrowseFragment.java615
-rw-r--r--src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java65
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java111
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java339
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsActivity.java96
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsFragment.java241
-rw-r--r--src/com/android/tv/dvr/ui/DvrDialogFragment.java50
-rw-r--r--src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java96
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java78
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java59
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java180
-rw-r--r--src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java71
-rw-r--r--src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java79
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java78
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java309
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java280
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java82
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java70
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java140
-rw-r--r--src/com/android/tv/dvr/ui/DvrSchedulesActivity.java94
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java49
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java52
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java126
-rw-r--r--src/com/android/tv/dvr/ui/EmptyItemPresenter.java66
-rw-r--r--src/com/android/tv/dvr/ui/FullScheduleCardHolder.java (renamed from src/com/android/tv/dvr/ui/EmptyHolder.java)12
-rw-r--r--src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java84
-rw-r--r--src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java56
-rw-r--r--src/com/android/tv/dvr/ui/PrioritySettingsFragment.java252
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java218
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramPresenter.java205
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java65
-rw-r--r--src/com/android/tv/dvr/ui/RecordingCardView.java91
-rw-r--r--src/com/android/tv/dvr/ui/RecordingDetailsFragment.java94
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java97
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java189
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java85
-rw-r--r--src/com/android/tv/dvr/ui/SeriesDeletionFragment.java249
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java271
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java238
-rw-r--r--src/com/android/tv/dvr/ui/SeriesSettingsFragment.java236
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java205
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java209
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java88
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java51
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java112
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java69
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java248
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java760
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java140
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java304
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java156
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java117
94 files changed, 16530 insertions, 1963 deletions
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 0fb469be..6af77940 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -20,17 +20,23 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.util.ArraySet;
import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.dvr.ScheduledRecording.RecordingState;
import com.android.tv.util.Clock;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* Base implementation of @{link DataManagerInternal}.
@@ -42,8 +48,14 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
private final static boolean DEBUG = false;
protected final Clock mClock;
+ private final Set<OnDvrScheduleLoadFinishedListener> mOnDvrScheduleLoadFinishedListeners =
+ new CopyOnWriteArraySet<>();
+ private final Set<OnRecordedProgramLoadFinishedListener>
+ mOnRecordedProgramLoadFinishedListeners = new CopyOnWriteArraySet<>();
private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>();
+ private final Set<SeriesRecordingListener> mSeriesRecordingListeners = new ArraySet<>();
private final Set<RecordedProgramListener> mRecordedProgramListeners = new ArraySet<>();
+ private final HashMap<Long, ScheduledRecording> mDeletedScheduleMap = new HashMap<>();
BaseDvrDataManager(Context context, Clock clock) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
@@ -51,6 +63,28 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public void addDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener) {
+ mOnDvrScheduleLoadFinishedListeners.add(listener);
+ }
+
+ @Override
+ public void removeDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener) {
+ mOnDvrScheduleLoadFinishedListeners.remove(listener);
+ }
+
+ @Override
+ public void addRecordedProgramLoadFinishedListener(
+ OnRecordedProgramLoadFinishedListener listener) {
+ mOnRecordedProgramLoadFinishedListeners.add(listener);
+ }
+
+ @Override
+ public void removeRecordedProgramLoadFinishedListener(
+ OnRecordedProgramLoadFinishedListener listener) {
+ mOnRecordedProgramLoadFinishedListeners.remove(listener);
+ }
+
+ @Override
public final void addScheduledRecordingListener(ScheduledRecordingListener listener) {
mScheduledRecordingListeners.add(listener);
}
@@ -61,6 +95,16 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public final void addSeriesRecordingListener(SeriesRecordingListener listener) {
+ mSeriesRecordingListeners.add(listener);
+ }
+
+ @Override
+ public final void removeSeriesRecordingListener(SeriesRecordingListener listener) {
+ mSeriesRecordingListeners.remove(listener);
+ }
+
+ @Override
public final void addRecordedProgramListener(RecordedProgramListener listener) {
mRecordedProgramListeners.add(listener);
}
@@ -71,6 +115,27 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
/**
+ * Calls {@link OnDvrScheduleLoadFinishedListener#onDvrScheduleLoadFinished} for each listener.
+ */
+ protected final void notifyDvrScheduleLoadFinished() {
+ for (OnDvrScheduleLoadFinishedListener l : mOnDvrScheduleLoadFinishedListeners) {
+ if (DEBUG) Log.d(TAG, "notify DVR schedule load finished");
+ l.onDvrScheduleLoadFinished();
+ }
+ }
+
+ /**
+ * Calls {@link OnRecordedProgramLoadFinishedListener#onRecordedProgramLoadFinished()}
+ * for each listener.
+ */
+ protected final void notifyRecordedProgramLoadFinished() {
+ for (OnRecordedProgramLoadFinishedListener l : mOnRecordedProgramLoadFinishedListeners) {
+ if (DEBUG) Log.d(TAG, "notify recorded programs load finished");
+ l.onRecordedProgramLoadFinished();
+ }
+ }
+
+ /**
* Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)}
* for each listener.
*/
@@ -104,10 +169,44 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
/**
- * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded(ScheduledRecording)}
+ * Calls {@link SeriesRecordingListener#onSeriesRecordingAdded}
+ * for each listener.
+ */
+ protected final void notifySeriesRecordingAdded(SeriesRecording... seriesRecordings) {
+ for (SeriesRecordingListener l : mSeriesRecordingListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "added " + seriesRecordings);
+ l.onSeriesRecordingAdded(seriesRecordings);
+ }
+ }
+
+ /**
+ * Calls {@link SeriesRecordingListener#onSeriesRecordingRemoved}
+ * for each listener.
+ */
+ protected final void notifySeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecordingListener l : mSeriesRecordingListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "removed " + seriesRecordings);
+ l.onSeriesRecordingRemoved(seriesRecordings);
+ }
+ }
+
+ /**
+ * Calls
+ * {@link SeriesRecordingListener#onSeriesRecordingChanged}
+ * for each listener.
+ */
+ protected final void notifySeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ for (SeriesRecordingListener l : mSeriesRecordingListeners) {
+ if (DEBUG) Log.d(TAG, "notify " + l + "changed " + seriesRecordings);
+ l.onSeriesRecordingChanged(seriesRecordings);
+ }
+ }
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded}
* for each listener.
*/
- protected final void notifyScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
+ protected final void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecording) {
for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording);
l.onScheduledRecordingAdded(scheduledRecording);
@@ -115,25 +214,23 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
/**
- * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved(ScheduledRecording)}
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved}
* for each listener.
*/
- protected final void notifyScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
+ 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 " + scheduledRecording);
l.onScheduledRecordingRemoved(scheduledRecording);
}
}
/**
* Calls
- * {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged(ScheduledRecording)}
+ * {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged}
* for each listener.
*/
protected final void notifyScheduledRecordingStatusChanged(
- ScheduledRecording scheduledRecording) {
+ ScheduledRecording... scheduledRecording) {
for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording);
l.onScheduledRecordingStatusChanged(scheduledRecording);
@@ -155,16 +252,74 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public List<ScheduledRecording> getAvailableScheduledRecordings() {
+ return filterEndTimeIsPast(getRecordingsWithState(
+ ScheduledRecording.STATE_RECORDING_IN_PROGRESS,
+ ScheduledRecording.STATE_RECORDING_NOT_STARTED));
+ }
+
+ @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));
+ return filterEndTimeIsPast(getRecordingsWithState(
+ ScheduledRecording.STATE_RECORDING_IN_PROGRESS));
}
@Override
public List<ScheduledRecording> getNonStartedScheduledRecordings() {
- return filterEndTimeIsPast(
- getRecordingsWithState(ScheduledRecording.STATE_RECORDING_NOT_STARTED));
+ Set<Integer> states = new HashSet<>();
+ states.add(ScheduledRecording.STATE_RECORDING_NOT_STARTED);
+ return filterEndTimeIsPast(getRecordingsWithState(
+ ScheduledRecording.STATE_RECORDING_NOT_STARTED));
+ }
+
+ @Override
+ public void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState) {
+ if (scheduledRecording.getState() != newState) {
+ updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording)
+ .setState(newState).build());
+ }
+ }
+
+ @Override
+ public Collection<ScheduledRecording> getDeletedSchedules() {
+ return mDeletedScheduleMap.values();
}
- protected abstract List<ScheduledRecording> getRecordingsWithState(int state);
+ @NonNull
+ @Override
+ public Collection<Long> getDisallowedProgramIds() {
+ return mDeletedScheduleMap.keySet();
+ }
+
+ /**
+ * Returns the map which contains the deleted schedules which are mapped from the program ID.
+ */
+ protected Map<Long, ScheduledRecording> getDeletedScheduleMap() {
+ return mDeletedScheduleMap;
+ }
+
+ /**
+ * Returns the schedules whose state is contained by states.
+ */
+ protected abstract List<ScheduledRecording> getRecordingsWithState(int... states);
+
+ @Override
+ public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) {
+ SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId);
+ List<RecordedProgram> result = new ArrayList<>();
+ for (RecordedProgram r : getRecordedPrograms()) {
+ if (seriesRecording.getSeriesId().equals(r.getSeriesId())) {
+ result.add(r);
+ }
+ }
+ return result;
+ }
}
diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/ConflictChecker.java
new file mode 100644
index 00000000..201e379e
--- /dev/null
+++ b/src/com/android/tv/dvr/ConflictChecker.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2015 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.ContentUris;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Message;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener;
+import com.android.tv.MainActivity;
+import com.android.tv.TvApplication;
+import com.android.tv.common.WeakHandler;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Checking the runtime conflict of DVR recording.
+ * <p>
+ * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+@MainThread
+public class ConflictChecker {
+ private static final String TAG = "ConflictChecker";
+ private static final boolean DEBUG = false;
+
+ private static final int MSG_CHECK_CONFLICT = 1;
+
+ private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30);
+
+ /**
+ * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
+ * less than or equal to this time.
+ */
+ private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5);
+ /**
+ * To show watch conflict dialog, the start time of the earliest conflicting schedule should be
+ * greater than or equal to this time.
+ */
+ private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30);
+
+ private final MainActivity mMainActivity;
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrScheduleManager mScheduleManager;
+ private final InputSessionManager mSessionManager;
+ private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this);
+
+ private final List<ScheduledRecording> mUpcomingConflicts = new ArrayList<>();
+ private final Set<OnUpcomingConflictChangeListener> mOnUpcomingConflictChangeListeners =
+ new ArraySet<>();
+ private final Map<Long, List<ScheduledRecording>> mCheckedConflictsMap = new HashMap<>();
+
+ private final ScheduledRecordingListener mScheduledRecordingListener =
+ new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings);
+ mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings);
+ mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings);
+ mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
+ }
+ };
+
+ private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener =
+ new OnTvViewChannelChangeListener() {
+ @Override
+ public void onTvViewChannelChange(@Nullable Uri channelUri) {
+ mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
+ }
+ };
+
+ private boolean mStarted;
+
+ public ConflictChecker(MainActivity mainActivity) {
+ mMainActivity = mainActivity;
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity);
+ mChannelDataManager = appSingletons.getChannelDataManager();
+ mScheduleManager = appSingletons.getDvrScheduleManager();
+ mSessionManager = appSingletons.getInputSessionManager();
+ }
+
+ /**
+ * Starts checking the conflict.
+ */
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+ mStarted = true;
+ mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT);
+ mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener);
+ mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
+ }
+
+ /**
+ * Stops checking the conflict.
+ */
+ public void stop() {
+ if (!mStarted) {
+ return;
+ }
+ mStarted = false;
+ mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener);
+ mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener);
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
+ /**
+ * Returns the upcoming conflicts.
+ */
+ public List<ScheduledRecording> getUpcomingConflicts() {
+ return new ArrayList<>(mUpcomingConflicts);
+ }
+
+ /**
+ * Adds a {@link OnUpcomingConflictChangeListener}.
+ */
+ public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
+ mOnUpcomingConflictChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes the {@link OnUpcomingConflictChangeListener}.
+ */
+ public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) {
+ mOnUpcomingConflictChangeListeners.remove(listener);
+ }
+
+ private void notifyUpcomingConflictChanged() {
+ for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) {
+ l.onUpcomingConflictChange();
+ }
+ }
+
+ /**
+ * Remembers the user's decision to record while watching the channel.
+ */
+ public void setCheckedConflictsForChannel(long mChannelId, List<ScheduledRecording> conflicts) {
+ mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts));
+ }
+
+ void onCheckConflict() {
+ // Checks the conflicting schedules and setup the next re-check time.
+ // If there are upcoming conflicts soon, it opens the conflict dialog.
+ if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT");
+ mHandler.removeMessages(MSG_CHECK_CONFLICT);
+ mUpcomingConflicts.clear();
+ if (!mScheduleManager.isInitialized()
+ || !mChannelDataManager.isDbLoadFinished()) {
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS);
+ notifyUpcomingConflictChanged();
+ return;
+ }
+ if (mSessionManager.getCurrentTvViewChannelUri() == null) {
+ // As MainActivity is not using a tuner, no need to check the conflict.
+ notifyUpcomingConflictChanged();
+ return;
+ }
+ Uri channelUri = mSessionManager.getCurrentTvViewChannelUri();
+ if (TvContract.isChannelUriForPassthroughInput(channelUri)) {
+ notifyUpcomingConflictChanged();
+ return;
+ }
+ long channelId = ContentUris.parseId(channelUri);
+ Channel channel = mChannelDataManager.getChannel(channelId);
+ // The conflicts caused by watching the channel.
+ List<ScheduledRecording> conflicts = mScheduleManager
+ .getConflictingSchedulesForWatching(channel.getId());
+ long earliestToCheck = Long.MAX_VALUE;
+ long currentTimeMs = System.currentTimeMillis();
+ for (ScheduledRecording schedule : conflicts) {
+ long startTimeMs = schedule.getStartTimeMs();
+ if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) {
+ // The start time of the upcoming conflict remains less than the minimum
+ // check time.
+ continue;
+ }
+ if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) {
+ // The start time of the upcoming conflict remains greater than the
+ // maximum check time. Setup the next re-check time.
+ long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS;
+ if (earliestToCheck > nextCheckTimeMs) {
+ earliestToCheck = nextCheckTimeMs;
+ }
+ } else {
+ // Found upcoming conflicts which will start soon.
+ mUpcomingConflicts.add(schedule);
+ // The schedule will be removed from the "upcoming conflict" when the
+ // recording is almost started.
+ long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS;
+ if (earliestToCheck > nextCheckTimeMs) {
+ earliestToCheck = nextCheckTimeMs;
+ }
+ }
+ }
+ if (earliestToCheck != Long.MAX_VALUE) {
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT,
+ earliestToCheck - currentTimeMs);
+ }
+ if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts);
+ notifyUpcomingConflictChanged();
+ if (!mUpcomingConflicts.isEmpty()
+ && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) {
+ // Don't show the conflict dialog if the user already knows.
+ List<ScheduledRecording> checkedConflicts = mCheckedConflictsMap.get(
+ channel.getId());
+ if (checkedConflicts == null
+ || !checkedConflicts.containsAll(mUpcomingConflicts)) {
+ DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel);
+ }
+ }
+ }
+
+ private static class ConflictCheckerHandler extends WeakHandler<ConflictChecker> {
+ ConflictCheckerHandler(ConflictChecker conflictChecker) {
+ super(conflictChecker);
+ }
+
+ @Override
+ protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) {
+ switch (msg.what) {
+ case MSG_CHECK_CONFLICT:
+ conflictChecker.onCheckConflict();
+ break;
+ }
+ }
+ }
+
+ /**
+ * A listener for the change of upcoming conflicts.
+ */
+ public interface OnUpcomingConflictChangeListener {
+ void onUpcomingConflictChange();
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index c96104e5..126f3e74 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -17,11 +17,13 @@
package com.android.tv.dvr;
import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Range;
-import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import java.util.Collection;
import java.util.List;
/**
@@ -34,16 +36,44 @@ public interface DvrDataManager {
boolean isInitialized();
/**
+ * Returns {@code true} if the schedules were loaded, otherwise {@code false}.
+ */
+ boolean isDvrScheduleLoadFinished();
+
+ /**
+ * Returns {@code true} if the recorded programs were loaded, otherwise {@code false}.
+ */
+ boolean isRecordedProgramLoadFinished();
+
+ /**
* Returns past recordings.
*/
List<RecordedProgram> getRecordedPrograms();
/**
+ * Returns past recorded programs in the given series.
+ */
+ List<RecordedProgram> getRecordedPrograms(long seriesRecordingId);
+
+ /**
* Returns all {@link ScheduledRecording} regardless of state.
+ * <p>
+ * The result doesn't contain the deleted schedules.
*/
List<ScheduledRecording> getAllScheduledRecordings();
/**
+ * Returns all available {@link ScheduledRecording}, it contains started and non started
+ * recordings.
+ */
+ List<ScheduledRecording> getAvailableScheduledRecordings();
+
+ /**
+ * Return all available and canceled {@link ScheduledRecording}.
+ */
+ List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings();
+
+ /**
* Returns started recordings that expired.
*/
List<ScheduledRecording> getStartedRecordings();
@@ -54,9 +84,14 @@ public interface DvrDataManager {
List<ScheduledRecording> getNonStartedScheduledRecordings();
/**
- * Returns season recordings.
+ * Returns series recordings.
*/
- List<SeasonRecording> getSeasonRecordings();
+ List<SeriesRecording> getSeriesRecordings();
+
+ /**
+ * Returns series recordings from the given input.
+ */
+ List<SeriesRecording> getSeriesRecordings(String inputId);
/**
* Returns the next start time after {@code time} or {@link #NEXT_START_TIME_NOT_FOUND}
@@ -67,15 +102,47 @@ public interface DvrDataManager {
long getNextScheduledStartTimeAfter(long time);
/**
- * Returns a list of all Recordings with a overlap with the given time period inclusive.
+ * Returns a list of the schedules with a overlap with the given time period inclusive and with
+ * the given state.
*
* <p> A recording overlaps with a period when
* {@code recording.getStartTime() <= period.getUpper() &&
* recording.getEndTime() >= period.getLower()}.
*
* @param period a time period in milliseconds.
+ * @param state the state of the schedule.
*/
- List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period);
+ List<ScheduledRecording> getScheduledRecordings(Range<Long> period, @RecordingState int state);
+
+ /**
+ * Returns a list of the schedules in the given series.
+ */
+ List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId);
+
+ /**
+ * Returns a list of the schedules from the given input.
+ */
+ List<ScheduledRecording> getScheduledRecordings(String inputId);
+
+ /**
+ * Add a {@link OnDvrScheduleLoadFinishedListener}.
+ */
+ void addDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener);
+
+ /**
+ * Remove a {@link OnDvrScheduleLoadFinishedListener}.
+ */
+ void removeDvrScheduleLoadFinishedListener(OnDvrScheduleLoadFinishedListener listener);
+
+ /**
+ * Add a {@link OnRecordedProgramLoadFinishedListener}.
+ */
+ void addRecordedProgramLoadFinishedListener(OnRecordedProgramLoadFinishedListener listener);
+
+ /**
+ * Remove a {@link OnRecordedProgramLoadFinishedListener}.
+ */
+ void removeRecordedProgramLoadFinishedListener(OnRecordedProgramLoadFinishedListener listener);
/**
* Add a {@link ScheduledRecordingListener}.
@@ -98,12 +165,21 @@ public interface DvrDataManager {
void removeRecordedProgramListener(RecordedProgramListener listener);
/**
+ * Add a {@link ScheduledRecordingListener}.
+ */
+ void addSeriesRecordingListener(SeriesRecordingListener seriesRecordingListener);
+
+ /**
+ * Remove a {@link ScheduledRecordingListener}.
+ */
+ void removeSeriesRecordingListener(SeriesRecordingListener seriesRecordingListener);
+
+ /**
* Returns the scheduled recording program with the given recordingId or null if is not found.
*/
@Nullable
ScheduledRecording getScheduledRecording(long recordingId);
-
/**
* Returns the scheduled recording program with the given programId or null if is not found.
*/
@@ -116,14 +192,73 @@ public interface DvrDataManager {
@Nullable
RecordedProgram getRecordedProgram(long recordingId);
+ /**
+ * Returns the series recording with the given seriesId or null if is not found.
+ */
+ @Nullable
+ SeriesRecording getSeriesRecording(long seriesRecordingId);
+
+ /**
+ * Returns the series recording with the given series ID or {@code null} if not found.
+ */
+ @Nullable
+ SeriesRecording getSeriesRecording(String seriesId);
+
+ /**
+ * Returns the schedules which are marked deleted.
+ */
+ Collection<ScheduledRecording> getDeletedSchedules();
+
+ /**
+ * Returns the program IDs which is not allowed to make a schedule automatically.
+ */
+ @NonNull
+ Collection<Long> getDisallowedProgramIds();
+
+ /**
+ * Listens for the DVR schedules loading finished.
+ */
+ interface OnDvrScheduleLoadFinishedListener {
+ void onDvrScheduleLoadFinished();
+ }
+
+ /**
+ * Listens for the recorded program loading finished.
+ */
+ interface OnRecordedProgramLoadFinishedListener {
+ void onRecordedProgramLoadFinished();
+ }
+
+ /**
+ * Listens for changes to {@link ScheduledRecording}s.
+ */
interface ScheduledRecordingListener {
- void onScheduledRecordingAdded(ScheduledRecording scheduledRecording);
+ void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings);
- void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording);
+ void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings);
- void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording);
+ /**
+ * Called when the schedules are updated.
+ *
+ * <p>Note that the passed arguments are the new objects with the same ID as the old ones.
+ */
+ void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings);
}
+ /**
+ * Listens for changes to {@link SeriesRecording}s.
+ */
+ interface SeriesRecordingListener {
+ void onSeriesRecordingAdded(SeriesRecording... seriesRecordings);
+
+ void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings);
+
+ void onSeriesRecordingChanged(SeriesRecording... seriesRecordings);
+ }
+
+ /**
+ * Listens for changes to {@link RecordedProgram}s.
+ */
interface RecordedProgramListener {
void onRecordedProgramAdded(RecordedProgram recordedProgram);
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 02c47750..5ae2c4ea 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -21,8 +21,7 @@ import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
-import android.database.Cursor;
-import android.media.tv.TvContract;
+import android.media.tv.TvContract.RecordedPrograms;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -31,23 +30,34 @@ import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Range;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.ScheduledRecording.RecordingState;
-import com.android.tv.dvr.provider.AsyncDvrDbTask;
-import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
-import com.android.tv.util.AsyncDbTask;
+import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
+import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
+import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask;
+import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask;
+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.AsyncRecordedProgramQueryTask;
import com.android.tv.util.Clock;
+import com.android.tv.util.TvProviderUriMatcher;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Map.Entry;
import java.util.Set;
/**
@@ -60,66 +70,67 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private static final boolean DEBUG = false;
private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
+ private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
+ private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>();
private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
new HashMap<>();
- private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
+ private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>();
private final Context mContext;
- private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) {
-
+ private final ContentObserver mContentObserver = new ContentObserver(new Handler(
+ Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
- public void onChange(boolean selfChange, @Nullable final Uri uri) {
- if (uri == null) {
- // TODO reload everything.
- }
- AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask(
+ public void onChange(boolean selfChange, final @Nullable Uri uri) {
+ RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(
mContext.getContentResolver(), uri);
task.executeOnDbThread();
mPendingTasks.add(task);
}
};
- private void onObservedChange(Uri uri, RecordedProgram recordedProgram) {
- long id = ContentUris.parseId(uri);
- if (DEBUG) {
- Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram);
- }
- if (recordedProgram == null) {
- RecordedProgram old = mRecordedPrograms.remove(id);
- if (old != null) {
- notifyRecordedProgramRemoved(old);
- } else {
- Log.w(TAG, "Could not find old version of deleted program #" + id);
- }
- } else {
- RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
- if (old == null) {
- notifyRecordedProgramAdded(recordedProgram);
- } else {
- notifyRecordedProgramChanged(recordedProgram);
- }
- }
- }
-
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
+ private final DvrDbSync mDbSync;
public DvrDataManagerImpl(Context context, Clock clock) {
super(context, clock);
mContext = context;
+ mDbSync = new DvrDbSync(context, this);
}
public void start() {
- AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) {
+ AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask
+ = new AsyncDvrQuerySeriesRecordingTask(mContext) {
+ @Override
+ protected void onCancelled(List<SeriesRecording> seriesRecordings) {
+ mPendingTasks.remove(this);
+ }
@Override
+ protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
+ mPendingTasks.remove(this);
+ long maxId = 0;
+ for (SeriesRecording r : seriesRecordings) {
+ mSeriesRecordings.put(r.getId(), r);
+ mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
+ }
+ IdGenerator.SERIES_RECORDING.setMaxId(maxId);
+ }
+ };
+ dvrQuerySeriesRecordingTask.executeOnDbThread();
+ mPendingTasks.add(dvrQuerySeriesRecordingTask);
+ AsyncDvrQueryScheduleTask dvrQueryRecordingTask
+ = new AsyncDvrQueryScheduleTask(mContext) {
+ @Override
protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
mPendingTasks.remove(this);
}
@@ -127,22 +138,63 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
protected void onPostExecute(List<ScheduledRecording> result) {
mPendingTasks.remove(this);
- mDvrLoadFinished = true;
+ long maxId = 0;
+ List<ScheduledRecording> toUpdate = new ArrayList<>();
for (ScheduledRecording r : result) {
- mScheduledRecordings.put(r.getId(), r);
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
+ getDeletedScheduleMap().put(r.getProgramId(), r);
+ } else {
+ mScheduledRecordings.put(r.getId(), r);
+ if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ 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());
+ }
+ }
+ }
+ if (maxId < r.getId()) {
+ maxId = r.getId();
+ }
+ }
+ if (!toUpdate.isEmpty()) {
+ updateScheduledRecording(true, ScheduledRecording.toArray(toUpdate));
+ }
+ IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
+ mDvrLoadFinished = true;
+ notifyDvrScheduleLoadFinished();
+ mDbSync.start();
+ if (isInitialized()) {
+ SeriesRecordingScheduler.getInstance(mContext).start();
}
}
};
- mDvrQueryTask.executeOnDbThread();
- mPendingTasks.add(mDvrQueryTask);
- AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask =
- new AsyncRecordedProgramsQueryTask(mContext.getContentResolver());
+ dvrQueryRecordingTask.executeOnDbThread();
+ mPendingTasks.add(dvrQueryRecordingTask);
+ RecordedProgramsQueryTask mRecordedProgramQueryTask =
+ new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
mRecordedProgramQueryTask.executeOnDbThread();
ContentResolver cr = mContext.getContentResolver();
- cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver);
+ cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
}
public void stop() {
+ SeriesRecordingScheduler.getInstance(mContext).stop();
+ mDbSync.stop();
ContentResolver cr = mContext.getContentResolver();
cr.unregisterContentObserver(mContentObserver);
Iterator<AsyncTask> i = mPendingTasks.iterator();
@@ -153,11 +205,80 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
+ private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
+ if (uri == null) {
+ uri = RecordedPrograms.CONTENT_URI;
+ }
+ int match = TvProviderUriMatcher.match(uri);
+ if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) {
+ if (!mRecordedProgramLoadFinished) {
+ for (RecordedProgram recorded : recordedPrograms) {
+ mRecordedPrograms.put(recorded.getId(), recorded);
+ }
+ mRecordedProgramLoadFinished = true;
+ notifyRecordedProgramLoadFinished();
+ } else if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ for (RecordedProgram recorded : mRecordedPrograms.values()) {
+ notifyRecordedProgramRemoved(recorded);
+ }
+ mRecordedPrograms.clear();
+ } else {
+ HashMap<Long, RecordedProgram> oldRecordedPrograms
+ = new HashMap<>(mRecordedPrograms);
+ mRecordedPrograms.clear();
+ for (RecordedProgram recorded : recordedPrograms) {
+ mRecordedPrograms.put(recorded.getId(), recorded);
+ RecordedProgram old = oldRecordedPrograms.remove(recorded.getId());
+ if (old == null) {
+ notifyRecordedProgramAdded(recorded);
+ } else {
+ notifyRecordedProgramChanged(recorded);
+ }
+ }
+ for (RecordedProgram recorded : oldRecordedPrograms.values()) {
+ notifyRecordedProgramRemoved(recorded);
+ }
+ }
+ if (isInitialized()) {
+ SeriesRecordingScheduler.getInstance(mContext).start();
+ }
+ } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
+ long id = ContentUris.parseId(uri);
+ if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
+ if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ RecordedProgram old = mRecordedPrograms.remove(id);
+ if (old != null) {
+ notifyRecordedProgramRemoved(old);
+ } else {
+ Log.w(TAG, "Could not find old version of deleted program #" + id);
+ }
+ } else {
+ RecordedProgram newRecorded = recordedPrograms.get(0);
+ RecordedProgram old = mRecordedPrograms.put(id, newRecorded);
+ if (old == null) {
+ notifyRecordedProgramAdded(newRecorded);
+ } else {
+ notifyRecordedProgramChanged(newRecorded);
+ }
+ }
+ }
+ }
+
@Override
public boolean isInitialized() {
return mDvrLoadFinished && mRecordedProgramLoadFinished;
}
+ @Override
+ public boolean isDvrScheduleLoadFinished() {
+ return mDvrLoadFinished;
+ }
+
+ @Override
+ public boolean isRecordedProgramLoadFinished() {
+ return mRecordedProgramLoadFinished;
+ }
+
private List<ScheduledRecording> getScheduledRecordingsPrograms() {
if (!mDvrLoadFinished) {
return Collections.emptyList();
@@ -177,24 +298,50 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
@Override
+ public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) {
+ SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId);
+ if (!mRecordedProgramLoadFinished || seriesRecording == null) {
+ return Collections.emptyList();
+ }
+ return super.getRecordedPrograms(seriesRecordingId);
+ }
+
+ @Override
public List<ScheduledRecording> getAllScheduledRecordings() {
return new ArrayList<>(mScheduledRecordings.values());
}
- protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) {
+ @Override
+ protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int... states) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
- if (r.getState() == state) {
- result.add(r);
+ for (int state : states) {
+ if (r.getState() == state) {
+ result.add(r);
+ break;
+ }
}
}
return result;
}
@Override
- public List<SeasonRecording> getSeasonRecordings() {
- // If we return dummy data here, we can implement UI part independently.
- return Collections.emptyList();
+ public List<SeriesRecording> getSeriesRecordings() {
+ if (!mDvrLoadFinished) {
+ return Collections.emptyList();
+ }
+ return new ArrayList<>(mSeriesRecordings.values());
+ }
+
+ @Override
+ public List<SeriesRecording> getSeriesRecordings(String inputId) {
+ List<SeriesRecording> result = new ArrayList<>();
+ for (SeriesRecording r : mSeriesRecordings.values()) {
+ if (TextUtils.equals(r.getInputId(), inputId)) {
+ result.add(r);
+ }
+ }
+ return result;
}
@Override
@@ -219,10 +366,33 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
@Override
- public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
+ public List<ScheduledRecording> getScheduledRecordings(Range<Long> period,
+ @RecordingState int state) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
- if (r.isOverLapping(period)) {
+ if (r.isOverLapping(period) && r.getState() == state) {
+ result.add(r);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId) {
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
+ if (r.getSeriesRecordingId() == seriesRecordingId) {
+ result.add(r);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public List<ScheduledRecording> getScheduledRecordings(String inputId) {
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
+ if (TextUtils.equals(r.getInputId(), inputId)) {
result.add(r);
}
}
@@ -232,19 +402,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Nullable
@Override
public ScheduledRecording getScheduledRecording(long recordingId) {
- if (mDvrLoadFinished) {
- return mScheduledRecordings.get(recordingId);
- }
- return null;
+ return mScheduledRecordings.get(recordingId);
}
@Nullable
@Override
public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
- if (mDvrLoadFinished) {
- return mProgramId2ScheduledRecordings.get(programId);
- }
- return null;
+ return mProgramId2ScheduledRecordings.get(programId);
}
@Nullable
@@ -253,151 +417,231 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return mRecordedPrograms.get(recordingId);
}
+ @Nullable
@Override
- public void addScheduledRecording(final ScheduledRecording scheduledRecording) {
- new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
- @Override
- protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) {
- super.onPostExecute(scheduledRecordings);
- SoftPreconditions.checkArgument(scheduledRecordings.size() == 1);
- for (ScheduledRecording r : scheduledRecordings) {
- if (r.getId() != -1) {
- mScheduledRecordings.put(r.getId(), r);
- if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
- }
- notifyScheduledRecordingAdded(r);
- } else {
- Log.w(TAG, "Error adding " + r);
- }
- }
+ public SeriesRecording getSeriesRecording(long seriesRecordingId) {
+ return mSeriesRecordings.get(seriesRecordingId);
+ }
+ @Nullable
+ @Override
+ public SeriesRecording getSeriesRecording(String seriesId) {
+ return mSeriesId2SeriesRecordings.get(seriesId);
+ }
+
+ @Override
+ public void addScheduledRecording(ScheduledRecording... schedules) {
+ for (ScheduledRecording r : schedules) {
+ r.setId(IdGenerator.SCHEDULED_RECORDING.newId());
+ mScheduledRecordings.put(r.getId(), r);
+ if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
- }.executeOnDbThread(scheduledRecording);
+ }
+ if (mDvrLoadFinished) {
+ notifyScheduledRecordingAdded(schedules);
+ }
+ new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules);
+ removeDeletedSchedules(schedules);
}
@Override
- public void addSeasonRecording(SeasonRecording seasonRecording) { }
+ public void addSeriesRecording(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording r : seriesRecordings) {
+ r.setId(IdGenerator.SERIES_RECORDING.newId());
+ mSeriesRecordings.put(r.getId(), r);
+ mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ }
+ if (mDvrLoadFinished) {
+ notifySeriesRecordingAdded(seriesRecordings);
+ }
+ new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ }
@Override
- public void removeScheduledRecording(final ScheduledRecording scheduledRecording) {
- new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
- @Override
- protected void onPostExecute(List<Integer> counts) {
- super.onPostExecute(counts);
- SoftPreconditions.checkArgument(counts.size() == 1);
- for (Integer c : counts) {
- if (c == 1) {
- mScheduledRecordings.remove(scheduledRecording.getId());
- if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings
- .remove(scheduledRecording.getProgramId());
- }
- //TODO change to notifyRecordingUpdated
- notifyScheduledRecordingRemoved(scheduledRecording);
- } else {
- Log.w(TAG, "Error removing " + scheduledRecording);
- }
- }
+ public void removeScheduledRecording(ScheduledRecording... schedules) {
+ removeScheduledRecording(false, schedules);
+ }
+ private void removeScheduledRecording(boolean forceDelete, 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());
}
- }.executeOnDbThread(scheduledRecording);
+ // 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) {
+ SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET);
+ ScheduledRecording deleted = ScheduledRecording.buildFrom(r)
+ .setState(ScheduledRecording.STATE_RECORDING_DELETED).build();
+ getDeletedScheduleMap().put(deleted.getProgramId(), deleted);
+ schedulesNotToDelete.add(deleted);
+ } else {
+ schedulesToDelete.add(r);
+ }
+ }
+ if (mDvrLoadFinished) {
+ notifyScheduledRecordingRemoved(schedules);
+ }
+ if (!schedulesToDelete.isEmpty()) {
+ new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
+ ScheduledRecording.toArray(schedulesToDelete));
+ }
+ if (!schedulesNotToDelete.isEmpty()) {
+ new AsyncUpdateScheduleTask(mContext).executeOnDbThread(
+ ScheduledRecording.toArray(schedulesNotToDelete));
+ }
}
@Override
- public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
+ public void removeSeriesRecording(final SeriesRecording... seriesRecordings) {
+ HashSet<Long> ids = new HashSet<>();
+ for (SeriesRecording r : seriesRecordings) {
+ mSeriesRecordings.remove(r.getId());
+ mSeriesId2SeriesRecordings.remove(r.getSeriesId());
+ ids.add(r.getId());
+ }
+ // Reset series recording ID of the scheduled recording.
+ List<ScheduledRecording> toUpdate = new ArrayList<>();
+ List<ScheduledRecording> toDelete = new ArrayList<>();
+ for (ScheduledRecording r : mScheduledRecordings.values()) {
+ if (ids.contains(r.getSeriesRecordingId())) {
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ toDelete.add(r);
+ } else {
+ toUpdate.add(ScheduledRecording.buildFrom(r)
+ .setSeriesRecordingId(SeriesRecording.ID_NOT_SET).build());
+ }
+ }
+ }
+ if (!toUpdate.isEmpty()) {
+ // No need to update DB. It's handled in database automatically when the series
+ // recording is deleted.
+ updateScheduledRecording(false, ScheduledRecording.toArray(toUpdate));
+ }
+ if (!toDelete.isEmpty()) {
+ removeScheduledRecording(true, ScheduledRecording.toArray(toDelete));
+ }
+ if (mDvrLoadFinished) {
+ notifySeriesRecordingRemoved(seriesRecordings);
+ }
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ removeDeletedSchedules(seriesRecordings);
+ }
@Override
- public void updateScheduledRecording(final ScheduledRecording scheduledRecording) {
- new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
- @Override
- protected void onPostExecute(List<Integer> counts) {
- super.onPostExecute(counts);
- SoftPreconditions.checkArgument(counts.size() == 1);
- for (Integer c : counts) {
- if (c == 1) {
- ScheduledRecording oldScheduledRecording = mScheduledRecordings
- .put(scheduledRecording.getId(), scheduledRecording);
- long programId = scheduledRecording.getProgramId();
- if (oldScheduledRecording != null
- && oldScheduledRecording.getProgramId() != programId
- && oldScheduledRecording.getProgramId()
- != ScheduledRecording.ID_NOT_SET) {
- ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
- .get(oldScheduledRecording.getProgramId());
- if (oldValueForProgramId.getId() == scheduledRecording.getId()) {
- //Only remove the old ScheduledRecording if it has the same ID as
- // the new one.
- mProgramId2ScheduledRecordings
- .remove(oldScheduledRecording.getProgramId());
- }
- }
- if (programId != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings.put(programId, scheduledRecording);
- }
- //TODO change to notifyRecordingUpdated
- notifyScheduledRecordingStatusChanged(scheduledRecording);
- } else {
- Log.w(TAG, "Error updating " + scheduledRecording);
- }
+ public void updateScheduledRecording(final ScheduledRecording... schedules) {
+ updateScheduledRecording(true, 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,
+ "Recording not found for: " + r)) {
+ continue;
+ }
+ toUpdate.add(r);
+ 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
+ && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
+ ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
+ .get(oldScheduledRecording.getProgramId());
+ if (oldValueForProgramId.getId() == r.getId()) {
+ // Only remove the old ScheduledRecording if it has the same ID as the new one.
+ mProgramId2ScheduledRecordings.remove(oldScheduledRecording.getProgramId());
}
}
- }.executeOnDbThread(scheduledRecording);
+ if (programId != ScheduledRecording.ID_NOT_SET) {
+ mProgramId2ScheduledRecordings.put(programId, r);
+ }
+ }
+ ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate);
+ if (mDvrLoadFinished) {
+ notifyScheduledRecordingStatusChanged(scheduleArray);
+ }
+ if (updateDb) {
+ new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
+ }
+ removeDeletedSchedules(scheduleArray);
}
- private final class AsyncRecordedProgramsQueryTask
- extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> {
- public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
- super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
- RecordedProgram.PROJECTION, null, null, null);
+ @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);
}
-
- @Override
- protected RecordedProgram fromCursor(Cursor c) {
- return RecordedProgram.fromCursor(c);
+ if (mDvrLoadFinished) {
+ notifySeriesRecordingChanged(seriesRecordings);
}
+ new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
+ }
- @Override
- protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
- mPendingTasks.remove(this);
+ private void removeDeletedSchedules(ScheduledRecording... addedSchedules) {
+ List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
+ for (ScheduledRecording r : addedSchedules) {
+ ScheduledRecording deleted = getDeletedScheduleMap().remove(r.getProgramId());
+ if (deleted != null) {
+ schedulesToDelete.add(deleted);
+ }
+ }
+ if (!schedulesToDelete.isEmpty()) {
+ new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
+ ScheduledRecording.toArray(schedulesToDelete));
}
+ }
- @Override
- protected void onPostExecute(List<RecordedProgram> result) {
- mPendingTasks.remove(this);
- mRecordedProgramLoadFinished = true;
- if (result != null) {
- for (RecordedProgram r : result) {
- mRecordedPrograms.put(r.getId(), r);
- }
+ private void removeDeletedSchedules(SeriesRecording... removedSeriesRecordings) {
+ Set<Long> seriesRecordingIds = new HashSet<>();
+ for (SeriesRecording r : removedSeriesRecordings) {
+ seriesRecordingIds.add(r.getId());
+ }
+ List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
+ Iterator<Entry<Long, ScheduledRecording>> iter =
+ getDeletedScheduleMap().entrySet().iterator();
+ while (iter.hasNext()) {
+ Entry<Long, ScheduledRecording> entry = iter.next();
+ if (seriesRecordingIds.contains(entry.getValue().getSeriesRecordingId())) {
+ schedulesToDelete.add(entry.getValue());
+ iter.remove();
}
}
+ if (!schedulesToDelete.isEmpty()) {
+ new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
+ ScheduledRecording.toArray(schedulesToDelete));
+ }
}
- private final class AsyncRecordedProgramQueryTask
- extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> {
-
+ private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
- public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
- super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
+ public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) {
+ super(contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri);
mUri = uri;
}
@Override
- protected RecordedProgram fromCursor(Cursor c) {
- return RecordedProgram.fromCursor(c);
- }
-
- @Override
- protected void onCancelled(RecordedProgram recordedProgram) {
+ protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
mPendingTasks.remove(this);
}
@Override
- protected void onPostExecute(RecordedProgram recordedProgram) {
+ protected void onPostExecute(List<RecordedProgram> result) {
mPendingTasks.remove(this);
- onObservedChange(mUri, recordedProgram);
+ onRecordedProgramsLoadedFinished(mUri, result);
}
}
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java b/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
deleted file mode 100644
index 95b342bb..00000000
--- a/src/com/android/tv/dvr/DvrDataManagerInMemoryImpl.java
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- * Copyright (C) 2015 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.Context;
-import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.util.Range;
-
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.recording.RecordedProgram;
-import com.android.tv.util.Clock;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * A DVR Data manager that stores values in memory suitable for testing.
- */
-@VisibleForTesting // TODO(DVR): move to testing dir.
-@MainThread
-public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager {
- private final static String TAG = "DvrDataManagerInMemory";
- private final AtomicLong mNextId = new AtomicLong(1);
- private final Map<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
- private final Map<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
- private final List<SeasonRecording> mSeasonSchedule = new ArrayList<>();
-
- public DvrDataManagerInMemoryImpl(Context context, Clock clock) {
- super(context, clock);
- }
-
- @Override
- public boolean isInitialized() {
- return true;
- }
-
- private List<ScheduledRecording> getScheduledRecordingsPrograms() {
- return new ArrayList(mScheduledRecordings.values());
- }
-
- @Override
- public List<RecordedProgram> getRecordedPrograms() {
- return new ArrayList<>(mRecordedPrograms.values());
- }
-
- @Override
- public List<ScheduledRecording> getAllScheduledRecordings() {
- return new ArrayList<>(mScheduledRecordings.values());
- }
-
- public List<SeasonRecording> getSeasonRecordings() {
- return mSeasonSchedule;
- }
-
- @Override
- public long getNextScheduledStartTimeAfter(long startTime) {
-
- List<ScheduledRecording> temp = getNonStartedScheduledRecordings();
- Collections.sort(temp, ScheduledRecording.START_TIME_COMPARATOR);
- for (ScheduledRecording r : temp) {
- if (r.getStartTimeMs() > startTime) {
- return r.getStartTimeMs();
- }
- }
- return DvrDataManager.NEXT_START_TIME_NOT_FOUND;
- }
-
- @Override
- public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
- List<ScheduledRecording> temp = getScheduledRecordingsPrograms();
- List<ScheduledRecording> result = new ArrayList<>();
- for (ScheduledRecording r : temp) {
- if (r.isOverLapping(period)) {
- result.add(r);
- }
- }
- return result;
- }
-
- /**
- * Add a new scheduled recording.
- */
- @Override
- public void addScheduledRecording(ScheduledRecording scheduledRecording) {
- addScheduledRecordingInternal(scheduledRecording);
- }
-
-
- public void addRecordedProgram(RecordedProgram recordedProgram) {
- addRecordedProgramInternal(recordedProgram);
- }
-
- public void updateRecordedProgram(RecordedProgram r) {
- long id = r.getId();
- if (mRecordedPrograms.containsKey(id)) {
- mRecordedPrograms.put(id, r);
- notifyRecordedProgramChanged(r);
- } else {
- throw new IllegalArgumentException("Recording not found:" + r);
- }
- }
-
- public void removeRecordedProgram(RecordedProgram scheduledRecording) {
- mRecordedPrograms.remove(scheduledRecording.getId());
- notifyRecordedProgramRemoved(scheduledRecording);
- }
-
-
- public ScheduledRecording addScheduledRecordingInternal(ScheduledRecording scheduledRecording) {
- SoftPreconditions
- .checkState(scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, TAG,
- "expected id of " + ScheduledRecording.ID_NOT_SET + " but was "
- + scheduledRecording);
- scheduledRecording = ScheduledRecording.buildFrom(scheduledRecording)
- .setId(mNextId.incrementAndGet())
- .build();
- mScheduledRecordings.put(scheduledRecording.getId(), scheduledRecording);
- notifyScheduledRecordingAdded(scheduledRecording);
- return scheduledRecording;
- }
-
- public RecordedProgram addRecordedProgramInternal(RecordedProgram recordedProgram) {
- SoftPreconditions.checkState(recordedProgram.getId() == RecordedProgram.ID_NOT_SET, TAG,
- "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram);
- recordedProgram = RecordedProgram.buildFrom(recordedProgram)
- .setId(mNextId.incrementAndGet())
- .build();
- mRecordedPrograms.put(recordedProgram.getId(), recordedProgram);
- notifyRecordedProgramAdded(recordedProgram);
- return recordedProgram;
- }
-
- @Override
- public void addSeasonRecording(SeasonRecording seasonRecording) {
- mSeasonSchedule.add(seasonRecording);
- }
-
- @Override
- public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
- mScheduledRecordings.remove(scheduledRecording.getId());
- notifyScheduledRecordingRemoved(scheduledRecording);
- }
-
- @Override
- public void removeSeasonSchedule(SeasonRecording seasonSchedule) {
- mSeasonSchedule.remove(seasonSchedule);
- }
-
- @Override
- public void updateScheduledRecording(ScheduledRecording r) {
- long id = r.getId();
- if (mScheduledRecordings.containsKey(id)) {
- mScheduledRecordings.put(id, r);
- notifyScheduledRecordingStatusChanged(r);
- } else {
- throw new IllegalArgumentException("Recording not found:" + r);
- }
- }
-
- @Nullable
- @Override
- public ScheduledRecording getScheduledRecording(long id) {
- return mScheduledRecordings.get(id);
- }
-
- @Nullable
- @Override
- public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
- for (ScheduledRecording r : mScheduledRecordings.values()) {
- if (r.getProgramId() == programId) {
- return r;
- }
- }
- return null;
- }
-
- @Nullable
- @Override
- public RecordedProgram getRecordedProgram(long recordingId) {
- return mRecordedPrograms.get(recordingId);
- }
-
- @Override
- @NonNull
- protected List<ScheduledRecording> getRecordingsWithState(int state) {
- ArrayList<ScheduledRecording> result = new ArrayList<>();
- for (ScheduledRecording r : mScheduledRecordings.values()) {
- if(r.getState() == state){
- result.add(r);
- }
- }
- return result;
- }
-}
diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java
new file mode 100644
index 00000000..baa7f3d9
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrDbSync.java
@@ -0,0 +1,207 @@
+/*
+ * 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.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.VisibleForTesting;
+
+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.LinkedList;
+import java.util.Objects;
+import java.util.Queue;
+
+/**
+ * A class to synchronizes DVR DB with TvProvider.
+ */
+@MainThread
+@TargetApi(Build.VERSION_CODES.N)
+class DvrDbSync {
+ private final Context mContext;
+ private final DvrDataManagerImpl mDataManager;
+ private UpdateProgramTask mUpdateProgramTask;
+ private final Queue<Long> mProgramIdQueue = new LinkedList<>();
+ private final ContentObserver mProgramsContentObserver = new ContentObserver(new Handler(
+ Looper.getMainLooper())) {
+ @SuppressLint("SwitchIntDef")
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ switch (TvProviderUriMatcher.match(uri)) {
+ case TvProviderUriMatcher.MATCH_PROGRAM:
+ onProgramsUpdated();
+ break;
+ case TvProviderUriMatcher.MATCH_PROGRAM_ID:
+ addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(
+ ContentUris.parseId(uri)));
+ break;
+ }
+ }
+ };
+ private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ addProgramIdToCheckIfNeeded(schedule);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ mProgramIdQueue.remove(schedule.getProgramId());
+ }
+ }
+ };
+
+ public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ mContext = context;
+ mDataManager = dataManager;
+ }
+
+ /**
+ * Starts the DB sync.
+ */
+ public void start() {
+ mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
+ mProgramsContentObserver);
+ mDataManager.addScheduledRecordingListener(mScheduleListener);
+ onProgramsUpdated();
+ }
+
+ /**
+ * Stops the DB sync.
+ */
+ public void stop() {
+ mProgramIdQueue.clear();
+ if (mUpdateProgramTask != null) {
+ mUpdateProgramTask.cancel(true);
+ }
+ mDataManager.removeScheduledRecordingListener(mScheduleListener);
+ mContext.getContentResolver().unregisterContentObserver(mProgramsContentObserver);
+ }
+
+ private void onProgramsUpdated() {
+ for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
+ addProgramIdToCheckIfNeeded(schedule);
+ }
+ }
+
+ private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) {
+ if (schedule == null) {
+ return;
+ }
+ long programId = schedule.getProgramId();
+ if (programId != ScheduledRecording.ID_NOT_SET
+ && !mProgramIdQueue.contains(programId)
+ && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ mProgramIdQueue.offer(programId);
+ startNextUpdateIfNeeded();
+ }
+ }
+
+ private void startNextUpdateIfNeeded() {
+ if (mProgramIdQueue.isEmpty()) {
+ return;
+ }
+ if (mUpdateProgramTask == null || mUpdateProgramTask.isCancelled()) {
+ mUpdateProgramTask = new UpdateProgramTask(mProgramIdQueue.poll());
+ mUpdateProgramTask.executeOnDbThread();
+ }
+ }
+
+ @VisibleForTesting
+ void handleUpdateProgram(Program program, long programId) {
+ 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);
+ } 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())
+ .setEpisodeNumber(program.getEpisodeNumber())
+ .setEpisodeTitle(program.getEpisodeTitle())
+ .setProgramDescription(program.getDescription())
+ .setProgramLongDescription(program.getLongDescription())
+ .setProgramPosterArtUri(program.getPosterArtUri())
+ .setProgramThumbnailUri(program.getThumbnailUri());
+ if (needToChangeStartTime) {
+ mDataManager.updateScheduledRecording(
+ builder.setStartTimeMs(program.getStartTimeUtcMillis()).build());
+ } else if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
+ || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
+ || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
+ || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
+ || !Objects.equals(schedule.getProgramDescription(),
+ program.getDescription())
+ || !Objects.equals(schedule.getProgramLongDescription(),
+ program.getLongDescription())
+ || !Objects.equals(schedule.getProgramPosterArtUri(),
+ program.getPosterArtUri())
+ || !Objects.equals(schedule.getProgramThumbnailUri(),
+ program.getThumbnailUri())) {
+ mDataManager.updateScheduledRecording(builder.build());
+ }
+ }
+ }
+ }
+
+ private class UpdateProgramTask extends AsyncQueryProgramTask {
+ private final long mProgramId;
+
+ public UpdateProgramTask(long programId) {
+ super(mContext.getContentResolver(), programId);
+ mProgramId = programId;
+ }
+
+ @Override
+ protected void onCancelled(Program program) {
+ mUpdateProgramTask = null;
+ startNextUpdateIfNeeded();
+ }
+
+ @Override
+ protected void onPostExecute(Program program) {
+ mUpdateProgramTask = null;
+ handleUpdateProgram(program, mProgramId);
+ startNextUpdateIfNeeded();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index e3dc622e..48ca6eee 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -16,28 +16,36 @@
package com.android.tv.dvr;
+import android.annotation.TargetApi;
import android.content.ContentResolver;
+import android.content.ContentUris;
import android.content.Context;
+import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
+import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
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 android.widget.Toast;
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.common.recording.RecordedProgram;
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.util.AsyncDbTask;
import com.android.tv.util.Utils;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -49,11 +57,14 @@ import java.util.Map.Entry;
* instead of modifying them directly through {@link DvrDataManager}.
*/
@MainThread
+@TargetApi(Build.VERSION_CODES.N)
public class DvrManager {
- private final static String TAG = "DvrManager";
+ private static final String TAG = "DvrManager";
+ private static final boolean DEBUG = false;
+
private final WritableDvrDataManager mDataManager;
private final ChannelDataManager mChannelDataManager;
- private final DvrSessionManager mDvrSessionManager;
+ private final DvrScheduleManager mScheduleManager;
// @GuardedBy("mListener")
private final Map<Listener, Handler> mListener = new HashMap<>();
private final Context mAppContext;
@@ -64,24 +75,58 @@ public class DvrManager {
mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
mAppContext = context.getApplicationContext();
mChannelDataManager = appSingletons.getChannelDataManager();
- mDvrSessionManager = appSingletons.getDvrSessionManger();
+ mScheduleManager = appSingletons.getDvrScheduleManager();
+ }
+
+ /**
+ * Schedules a recording for {@code program}.
+ */
+ public void 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;
+ }
+ 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);
}
/**
* 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
*/
public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) {
- Log.i(TAG,
- "Adding scheduled recording of " + program + " instead of " + recordingsToOverride);
+ Log.i(TAG, "Adding scheduled recording of " + program + " instead of " +
+ recordingsToOverride);
+ 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;
+ }
Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR);
- Channel c = mChannelDataManager.getChannel(program.getChannelId());
long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE
- : recordingsToOverride.get(0).getPriority() - 1;
- ScheduledRecording r = ScheduledRecording.builder(program)
+ : recordingsToOverride.get(0).getPriority() + 1;
+ ScheduledRecording r = createScheduledRecordingBuilder(input.getId(), program)
.setPriority(priority)
- .setChannelId(c.getId())
.build();
mDataManager.addScheduledRecording(r);
}
@@ -90,27 +135,242 @@ public class DvrManager {
* Adds a recording schedule with a time range.
*/
public void addSchedule(Channel channel, long startTime, long endTime) {
- Log.i(TAG, "Adding scheduled recording of channel" + channel + " starting at " +
+ Log.i(TAG, "Adding scheduled recording of channel " + channel + " starting at " +
Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime));
- //TODO: handle error cases
- ScheduledRecording r = ScheduledRecording.builder(startTime, endTime)
- .setChannelId(channel.getId())
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return;
+ }
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
+ if (input == null) {
+ Log.e(TAG, "Can't find input for channel: " + channel);
+ return;
+ }
+ addScheduleInternal(input.getId(), channel.getId(), startTime, endTime);
+ }
+
+ private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) {
+ mDataManager.addScheduledRecording(ScheduledRecording
+ .builder(inputId, channelId, startTime, endTime)
+ .setPriority(mScheduleManager.suggestNewPriority())
+ .build());
+ }
+
+ /**
+ * Adds a new series recording and schedules for the programs.
+ */
+ public SeriesRecording addSeriesRecording(Program selectedProgram,
+ List<Program> programsToSchedule) {
+ Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: "
+ + programsToSchedule);
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
+ return null;
+ }
+ TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, selectedProgram);
+ if (input == null) {
+ Log.e(TAG, "Can't find input for program: " + selectedProgram);
+ return null;
+ }
+ SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram)
+ .setPriority(mScheduleManager.suggestNewSeriesPriority())
.build();
- mDataManager.addScheduledRecording(r);
+ mDataManager.addSeriesRecording(seriesRecording);
+ // The schedules for the recorded programs should be added not to create the schedule the
+ // duplicate episodes.
+ addRecordedProgramToSeriesRecording(seriesRecording);
+ addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
+ return seriesRecording;
+ }
+
+ private void addRecordedProgramToSeriesRecording(SeriesRecording series) {
+ List<ScheduledRecording> toAdd = new ArrayList<>();
+ for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) {
+ if (series.getSeriesId().equals(recordedProgram.getSeriesId())
+ && !recordedProgram.isClipped()) {
+ // Duplicate schedules can exist, but they will be deleted in a few days. And it's
+ // also guaranteed that the schedules don't belong to any series recordings because
+ // there are no more than one series recordings which have the same program title.
+ toAdd.add(ScheduledRecording.builder(recordedProgram)
+ .setPriority(series.getPriority())
+ .setSeriesRecordingId(series.getId()).build());
+ }
+ }
+ if (!toAdd.isEmpty()) {
+ mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
+ }
+ }
+
+ /**
+ * Adds {@link ScheduledRecording}s for the series recording.
+ * <p>
+ * This method doesn't add the series recording.
+ */
+ public void addScheduleToSeriesRecording(SeriesRecording series,
+ List<Program> programsToSchedule) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return;
+ }
+ TvInputInfo input = Utils.getTvInputInfoForInputId(mAppContext, series.getInputId());
+ if (input == null) {
+ Log.e(TAG, "Can't find input with ID: " + series.getInputId());
+ return;
+ }
+ List<ScheduledRecording> toAdd = new ArrayList<>();
+ List<ScheduledRecording> toUpdate = new ArrayList<>();
+ for (Program program : programsToSchedule) {
+ ScheduledRecording scheduleWithSameProgram =
+ mDataManager.getScheduledRecordingForProgramId(program.getId());
+ if (scheduleWithSameProgram != null) {
+ if (scheduleWithSameProgram.getState()
+ == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || scheduleWithSameProgram.getState()
+ == ScheduledRecording.STATE_RECORDING_CANCELED) {
+ ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram)
+ .setPriority(series.getPriority())
+ .setSeriesRecordingId(series.getId())
+ .build();
+ if (!r.equals(scheduleWithSameProgram)) {
+ toUpdate.add(r);
+ }
+ }
+ } else {
+ ScheduledRecording.Builder scheduledRecordingBuilder =
+ 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());
+ }
+ }
+ if (!toAdd.isEmpty()) {
+ mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
+ }
+ if (!toUpdate.isEmpty()) {
+ mDataManager.updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
+ }
+ }
+
+ /**
+ * Updates the series recording.
+ */
+ public void updateSeriesRecording(SeriesRecording series) {
+ if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ // TODO: revise this method. b/30946239
+ boolean isPreviousCanceled = false;
+ long oldPriority = 0;
+ SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
+ if (previousSeries != null) {
+ isPreviousCanceled = previousSeries.getState()
+ == SeriesRecording.STATE_SERIES_CANCELED;
+ oldPriority = previousSeries.getPriority();
+ }
+ 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()) {
+ 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()) {
+ schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule)
+ .setPriority(priority).build());
+ }
+ }
+ if (!schedulesToUpdate.isEmpty()) {
+ mDataManager.updateScheduledRecording(
+ ScheduledRecording.toArray(schedulesToUpdate));
+ }
+ }
+ }
+ }
+
+ 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());
+ }
+ }
+ }
}
/**
- * Adds a season recording schedule based on {@code program}.
+ * 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 addSeasonSchedule(Program program) {
- Log.i(TAG, "Adding season recording of " + program);
- // TODO: implement
+ 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.
+ */
+ public void removeSeriesRecording(long seriesRecordingId) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return;
+ }
+ SeriesRecording series = mDataManager.getSeriesRecording(seriesRecordingId);
+ if (series == null) {
+ return;
+ }
+ for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
+ if (schedule.getSeriesRecordingId() == seriesRecordingId) {
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ stopRecording(schedule);
+ break;
+ }
+ }
+ }
+ mDataManager.removeSeriesRecording(series);
}
/**
* Stops the currently recorded program
*/
public void stopRecording(final ScheduledRecording recording) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return;
+ }
synchronized (mListener) {
for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
entry.getValue().post(new Runnable() {
@@ -124,86 +384,258 @@ public class DvrManager {
}
/**
- * Removes a scheduled recording or an existing recording.
+ * Removes scheduled recordings or an existing recordings.
*/
- public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
- Log.i(TAG, "Removing " + scheduledRecording);
- mDataManager.removeScheduledRecording(scheduledRecording);
+ public void removeScheduledRecording(ScheduledRecording... schedules) {
+ Log.i(TAG, "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(r);
+ }
+ }
+ }
+
+ /**
+ * Removes the recorded program. It deletes the file if possible.
+ */
+ public void removeRecordedProgram(Uri recordedProgramUri) {
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
+ return;
+ }
+ removeRecordedProgram(ContentUris.parseId(recordedProgramUri));
+ }
+
+ /**
+ * Removes the recorded program. It deletes the file if possible.
+ */
+ public void removeRecordedProgram(long recordedProgramId) {
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
+ return;
+ }
+ RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
+ if (recordedProgram != null) {
+ removeRecordedProgram(recordedProgram);
+ }
}
+ /**
+ * Removes the recorded program. It deletes the file if possible.
+ */
public void removeRecordedProgram(final RecordedProgram recordedProgram) {
- // TODO(dvr): implement
- Log.i(TAG, "To delete " + recordedProgram
- + "\nyou should manually delete video data at"
- + "\nadb shell rm -rf " + recordedProgram.getDataUri()
- );
- Toast.makeText(mAppContext, "Deleting recorded programs is not fully implemented yet",
- Toast.LENGTH_SHORT).show();
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
+ return;
+ }
new AsyncDbTask<Void, Void, Void>() {
@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());
+ }
+ }
+ }
+ } catch (SecurityException e) {
+ if (DEBUG) {
+ Log.d(TAG, "To delete " + recordedProgram
+ + "\nyou should manually delete video data at"
+ + "\nadb shell rm -rf " + recordedProgram.getDataUri());
+ }
+ }
return null;
}
- }.execute();
+ }.executeOnDbThread();
}
/**
- * Returns priority ordered list of all scheduled recording that will not be recorded if
+ * 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;
+ }
+ 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);
+ return null;
+ }
+ }.executeOnDbThread();
+ }
+
+ /**
+ * Updates the scheduled recording.
+ */
+ public void updateScheduledRecording(ScheduledRecording recording) {
+ if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ mDataManager.updateScheduledRecording(recording);
+ }
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
* this program is.
*
- * <p>Any empty list means there is no conflicts. If there is conflict the program must be
- * scheduled to record with a Priority lower than the first Recording in the list returned.
- */
- public List<ScheduledRecording> getScheduledRecordingsThatConflict(Program program) {
- //TODO(DVR): move to scheduler.
- //TODO(DVR): deal with more than one DvrInputService
- List<ScheduledRecording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program));
- if (!overLap.isEmpty()) {
- // TODO(DVR): ignore shows that already won't record.
- Channel channel = mChannelDataManager.getChannel(program.getChannelId());
- if (channel != null) {
- TvInputInfo info = mDvrSessionManager.getTvInputInfo(channel.getInputId());
- if (info == null) {
- Log.w(TAG,
- "Could not find a recording TvInputInfo for " + channel.getInputId());
- return overLap;
- }
- int remove = Math.max(0, info.getTunerCount() - 1);
- if (remove >= overLap.size()) {
- return Collections.EMPTY_LIST;
- }
- overLap = overLap.subList(remove, overLap.size() - 1);
+ * @see DvrScheduleManager#getConflictingSchedules(Program)
+ */
+ public List<ScheduledRecording> getConflictingSchedules(Program program) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return Collections.emptyList();
+ }
+ return mScheduleManager.getConflictingSchedules(program);
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
+ * this channel is.
+ *
+ * @see DvrScheduleManager#getConflictingSchedules(long, long, long)
+ */
+ public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs,
+ long endTimeMs) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return Collections.emptyList();
+ }
+ return mScheduleManager.getConflictingSchedules(channelId, startTimeMs, endTimeMs);
+ }
+
+ /**
+ * Checks if the schedule is conflicting.
+ *
+ * <p>Note that the {@code schedule} should be the existing one. If not, this returns
+ * {@code false}.
+ */
+ public boolean isConflicting(ScheduledRecording schedule) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return false;
+ }
+ return mScheduleManager.isConflicting(schedule);
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recording that will not be recorded if
+ * this channel is tuned to.
+ *
+ * @see DvrScheduleManager#getConflictingSchedulesForTune
+ */
+ public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return Collections.emptyList();
+ }
+ return mScheduleManager.getConflictingSchedulesForTune(channelId);
+ }
+
+ /**
+ * Returns the earliest end time of the current recording for the TV input. If there are no
+ * recordings, Long.MAX_VALUE is returned.
+ */
+ 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();
}
}
- return overLap;
+ return result;
}
- @NonNull
- private static Range getPeriod(Program program) {
- return new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis());
+ /**
+ * Returns {@code true} if the channel can be recorded.
+ * <p>
+ * Note that this method doesn't check the conflict of the schedule or available tuners.
+ * This can be called from the UI before the schedules are loaded.
+ */
+ public boolean isChannelRecordable(Channel channel) {
+ if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) {
+ return false;
+ }
+ TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
+ if (info == null) {
+ Log.w(TAG, "Could not find TvInputInfo for " + channel);
+ return false;
+ }
+ if (!info.canRecord()) {
+ return false;
+ }
+ Program program = TvApplication.getSingletons(mAppContext).getProgramDataManager()
+ .getCurrentProgram(channel.getId());
+ return program == null || !program.isRecordingProhibited();
}
/**
- * Checks whether {@code channel} can be tuned without any conflict with existing recordings
- * in progress. If there is any conflict, {@code outConflictRecordings} will be filled.
+ * Returns {@code true} if the program can be recorded.
+ * <p>
+ * Note that this method doesn't check the conflict of the schedule or available tuners.
+ * This can be called from the UI before the schedules are loaded.
*/
- public boolean canTuneTo(Channel channel, List<ScheduledRecording> outConflictScheduledRecordings) {
- // TODO: implement
- return true;
+ public boolean isProgramRecordable(Program program) {
+ if (!mDataManager.isInitialized()) {
+ return false;
+ }
+ TvInputInfo info = Utils.getTvInputInfoForProgram(mAppContext, program);
+ if (info == null) {
+ Log.w(TAG, "Could not find TvInputInfo for " + program);
+ return false;
+ }
+ return info.canRecord() && !program.isRecordingProhibited();
}
/**
- * Returns true is the inputId supports recording.
+ * Returns the current recording for the channel.
+ * <p>
+ * This can be called from the UI before the schedules are loaded.
*/
- public boolean canRecord(String inputId) {
- TvInputInfo info = mDvrSessionManager.getTvInputInfo(inputId);
- return info != null && info.getTunerCount() > 0;
+ public ScheduledRecording getCurrentRecording(long channelId) {
+ if (!mDataManager.isDvrScheduleLoadFinished()) {
+ return null;
+ }
+ for (ScheduledRecording recording : mDataManager.getStartedRecordings()) {
+ if (recording.getChannelId() == channelId) {
+ return recording;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the series recording related to the program.
+ */
+ @Nullable
+ public SeriesRecording getSeriesRecording(Program program) {
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return null;
+ }
+ return mDataManager.getSeriesRecording(program.getSeriesId());
}
@WorkerThread
- void addListener(Listener listener, @NonNull Handler handler) {
+ @VisibleForTesting
+ // Should be public to use mock DvrManager object.
+ public void addListener(Listener listener, @NonNull Handler handler) {
SoftPreconditions.checkNotNull(handler);
synchronized (mListener) {
mListener.put(listener, handler);
@@ -211,13 +643,68 @@ public class DvrManager {
}
@WorkerThread
- void removeListener(Listener listener) {
+ @VisibleForTesting
+ // Should be public to use mock DvrManager object.
+ public void removeListener(Listener listener) {
synchronized (mListener) {
mListener.remove(listener);
}
}
/**
+ * Returns ScheduledRecording.builder based on {@code program}. If program is already started,
+ * recording started time is clipped to the current time.
+ */
+ private ScheduledRecording.Builder createScheduledRecordingBuilder(String inputId,
+ Program program) {
+ ScheduledRecording.Builder builder = ScheduledRecording.builder(inputId, program);
+ long time = System.currentTimeMillis();
+ if (program.getStartTimeUtcMillis() < time && time < program.getEndTimeUtcMillis()) {
+ builder.setStartTimeMs(time);
+ }
+ return builder;
+ }
+
+ /**
+ * Returns a schedule which matches to the given episode.
+ */
+ public ScheduledRecording getScheduledRecording(String title, String seasonNumber,
+ String episodeNumber) {
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null
+ || seasonNumber == null || episodeNumber == null) {
+ return null;
+ }
+ for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
+ if (title.equals(r.getProgramTitle())
+ && seasonNumber.equals(r.getSeasonNumber())
+ && episodeNumber.equals(r.getEpisodeNumber())) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a recorded program which is the same episode as the given {@code program}.
+ */
+ public RecordedProgram getRecordedProgram(String title, String seasonNumber,
+ String episodeNumber) {
+ if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null
+ || seasonNumber == null || episodeNumber == null) {
+ return null;
+ }
+ for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
+ if (title.equals(r.getTitle())
+ && seasonNumber.equals(r.getSeasonNumber())
+ && episodeNumber.equals(r.getEpisodeNumber())
+ && !r.isClipped()) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ /**
* Listener internally used inside dvr package.
*/
interface Listener {
diff --git a/src/com/android/tv/dvr/DvrPlayActivity.java b/src/com/android/tv/dvr/DvrPlayActivity.java
deleted file mode 100644
index b117a7cf..00000000
--- a/src/com/android/tv/dvr/DvrPlayActivity.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2015 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.app.Activity;
-import android.os.Bundle;
-import android.widget.TextView;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-
-/**
- * Simple Activity to play a {@link ScheduledRecording}.
- */
-public class DvrPlayActivity extends Activity {
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.dvr_play);
-
- DvrDataManager dvrDataManager = TvApplication.getSingletons(this).getDvrDataManager();
- // TODO(DVR) handle errors.
- long recordingId = getIntent().getLongExtra(ScheduledRecording.RECORDING_ID_EXTRA, 0);
- ScheduledRecording scheduledRecording = dvrDataManager.getScheduledRecording(recordingId);
- TextView textView = (TextView) findViewById(R.id.placeHolderText);
- if (scheduledRecording != null) {
- textView.setText(scheduledRecording.toString());
- } else {
- textView.setText(R.string.ut_result_not_found_title); // TODO(DVR) update error text
- }
- }
-} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java
new file mode 100644
index 00000000..3320e0fd
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrPlaybackActivity.java
@@ -0,0 +1,65 @@
+/*
+ * 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.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.tv.R;
+import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
+
+/**
+ * Activity to play a {@link RecordedProgram}.
+ */
+public class DvrPlaybackActivity extends Activity {
+ private static final String TAG = "DvrPlaybackActivity";
+ private static final boolean DEBUG = false;
+
+ private DvrPlaybackOverlayFragment mOverlayFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_playback);
+ mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager()
+ .findFragmentById(R.id.dvr_playback_controls_fragment);
+ }
+
+ @Override
+ public void onVisibleBehindCanceled() {
+ if (DEBUG) Log.d(TAG, "onVisibleBehindCanceled");
+ super.onVisibleBehindCanceled();
+ finish();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ mOverlayFragment.onNewIntent(intent);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ float density = getResources().getDisplayMetrics().density;
+ mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density),
+ (int) (newConfig.screenHeightDp * density));
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
new file mode 100644
index 00000000..da815712
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
@@ -0,0 +1,297 @@
+/*
+ * 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.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.media.tv.TvContract;
+import android.os.AsyncTask;
+import android.support.annotation.Nullable;
+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.util.ImageLoader;
+import com.android.tv.util.TimeShiftUtils;
+
+public class DvrPlaybackMediaSessionHelper {
+ private static final String TAG = "DvrPlaybackMediaSessionHelper";
+ private static final boolean DEBUG = false;
+
+ private int mNowPlayingCardWidth;
+ private int mNowPlayingCardHeight;
+ private int mSpeedLevel;
+ private long mProgramDurationMs;
+
+ private Activity mActivity;
+ private DvrPlayer mDvrPlayer;
+ private MediaSession mMediaSession;
+ private final DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private final ChannelDataManager mChannelDataManager;
+
+ public DvrPlaybackMediaSessionHelper(Activity activity,
+ String mediaSessionTag, DvrPlayer dvrPlayer) {
+ mActivity = activity;
+ mDvrPlayer = dvrPlayer;
+ mDvrWatchedPositionManager =
+ TvApplication.getSingletons(activity).getDvrWatchedPositionManager();
+ mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager();
+ mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() {
+ @Override
+ public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {
+ updateMediaSessionPlaybackState();
+ }
+
+ @Override
+ public void onPlaybackPositionChanged(long positionMs) {
+ updateMediaSessionPlaybackState();
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrWatchedPositionManager
+ .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs);
+ }
+ }
+ });
+ initializeMediaSession(mediaSessionTag);
+ }
+
+ /**
+ * Stops DVR player and release media session.
+ */
+ public void release() {
+ if (mDvrPlayer != null) {
+ mDvrPlayer.reset();
+ }
+ if (mMediaSession != null) {
+ mMediaSession.release();
+ }
+ }
+
+ /**
+ * Updates media session's playback state and speed.
+ */
+ public void updateMediaSessionPlaybackState() {
+ mMediaSession.setPlaybackState(new PlaybackState.Builder()
+ .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(),
+ mSpeedLevel).build());
+ }
+
+ /**
+ * Sets the recorded program for playback.
+ *
+ * @param program The recorded program to play. {@code null} to reset the DVR player.
+ */
+ public void setupPlayback(RecordedProgram program, long seekPositionMs) {
+ if (program != null) {
+ mDvrPlayer.setProgram(program, seekPositionMs);
+ setupMediaSession(program);
+ } else {
+ mDvrPlayer.reset();
+ mMediaSession.setActive(false);
+ }
+ }
+
+ /**
+ * Returns the recorded program now playing.
+ */
+ public RecordedProgram getProgram() {
+ return mDvrPlayer.getProgram();
+ }
+
+ /**
+ * Checks if the recorded program is the same as now playing one.
+ */
+ public boolean isCurrentProgram(RecordedProgram program) {
+ return program == null ? false : program.equals(getProgram());
+ }
+
+ /**
+ * Returns playback state.
+ */
+ public int getPlaybackState() {
+ return mDvrPlayer.getPlaybackState();
+ }
+
+ /**
+ * Returns the underlying DVR player.
+ */
+ public DvrPlayer getDvrPlayer() {
+ return mDvrPlayer;
+ }
+
+ private void initializeMediaSession(String mediaSessionTag) {
+ mMediaSession = new MediaSession(mActivity, mediaSessionTag);
+ mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
+ | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mNowPlayingCardWidth = mActivity.getResources()
+ .getDimensionPixelSize(R.dimen.notif_card_img_max_width);
+ mNowPlayingCardHeight = mActivity.getResources()
+ .getDimensionPixelSize(R.dimen.notif_card_img_height);
+ mMediaSession.setCallback(new MediaSessionCallback());
+ mActivity.setMediaController(
+ new MediaController(mActivity, mMediaSession.getSessionToken()));
+ updateMediaSessionPlaybackState();
+ }
+
+ private void setupMediaSession(RecordedProgram program) {
+ mProgramDurationMs = program.getDurationMillis();
+ String cardTitleText = program.getTitle();
+ if (TextUtils.isEmpty(cardTitleText)) {
+ Channel channel = mChannelDataManager.getChannel(program.getChannelId());
+ cardTitleText = (channel != null) ? channel.getDisplayName()
+ : mActivity.getString(R.string.no_program_information);
+ }
+ updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(),
+ mProgramDurationMs, null, 0);
+ String posterArtUri = program.getPosterArtUri();
+ if (posterArtUri == null) {
+ posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString();
+ }
+ updatePosterArt(program, cardTitleText, program.getDescription(),
+ mProgramDurationMs, null, posterArtUri);
+ mMediaSession.setActive(true);
+ }
+
+ private void updatePosterArt(RecordedProgram program, String cardTitleText,
+ String cardSubtitleText, long duration,
+ @Nullable Bitmap posterArt, @Nullable String posterArtUri) {
+ if (posterArt != null) {
+ updateMediaMetadata(program.getId(), cardTitleText,
+ cardSubtitleText, duration, posterArt, 0);
+ } else if (posterArtUri != null) {
+ ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth,
+ mNowPlayingCardHeight, new ProgramPosterArtCallback(
+ mActivity, program, cardTitleText, cardSubtitleText, duration));
+ } else {
+ updateMediaMetadata(program.getId(), cardTitleText,
+ cardSubtitleText, duration, null, R.drawable.default_now_card);
+ }
+ }
+
+ private class ProgramPosterArtCallback extends
+ ImageLoader.ImageLoaderCallback<Activity> {
+ private RecordedProgram mRecordedProgram;
+ private String mCardTitleText;
+ private String mCardSubtitleText;
+ private long mDuration;
+
+ public ProgramPosterArtCallback(Activity activity, RecordedProgram program,
+ String cardTitleText, String cardSubtitleText, long duration) {
+ super(activity);
+ mRecordedProgram = program;
+ mCardTitleText = cardTitleText;
+ mCardSubtitleText = cardSubtitleText;
+ mDuration = duration;
+ }
+
+ @Override
+ public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) {
+ if (isCurrentProgram(mRecordedProgram)) {
+ updatePosterArt(mRecordedProgram, mCardTitleText,
+ mCardSubtitleText, mDuration, posterArt, null);
+ }
+ }
+ }
+
+ private void updateMediaMetadata(final long programId, final String title,
+ final String subtitle, final long duration,
+ final Bitmap posterArt, final int imageResId) {
+ new AsyncTask<Void, Void, Void> () {
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId)
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
+ if (subtitle != null) {
+ builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+ }
+ Bitmap programPosterArt = posterArt;
+ if (programPosterArt == null && imageResId != 0) {
+ programPosterArt =
+ BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
+ }
+ if (programPosterArt != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
+ }
+ mMediaSession.setMetadata(builder.build());
+ return null;
+ }
+ }.execute();
+ }
+
+ // An event was triggered by MediaController.TransportControls and must be handled here.
+ // Here we update the media itself to act on the event that was triggered.
+ private class MediaSessionCallback extends MediaSession.Callback {
+ @Override
+ public void onPrepare() {
+ if (!mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.prepare(true);
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ mDvrPlayer.play();
+ }
+
+ @Override
+ public void onPause() {
+ mDvrPlayer.pause();
+ }
+
+ @Override
+ public void onFastForward() {
+ if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) {
+ if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
+ mSpeedLevel++;
+ } else {
+ return;
+ }
+ } else {
+ mSpeedLevel = 0;
+ }
+ mDvrPlayer.fastForward(
+ TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs));
+ }
+
+ @Override
+ public void onRewind() {
+ if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) {
+ if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
+ mSpeedLevel++;
+ } else {
+ return;
+ }
+ } else {
+ mSpeedLevel = 0;
+ }
+ mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs));
+ }
+
+ @Override
+ public void onSeekTo(long positionMs) {
+ mDvrPlayer.seekTo(positionMs);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java
new file mode 100644
index 00000000..027d99f4
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrPlayer.java
@@ -0,0 +1,393 @@
+/*
+ * 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.media.PlaybackParams;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.media.session.PlaybackState;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class DvrPlayer {
+ private static final String TAG = "DvrPlayer";
+ private static final boolean DEBUG = false;
+
+ /**
+ * The max rewinding speed supported by DVR player.
+ */
+ public static final int MAX_REWIND_SPEED = 256;
+ /**
+ * The max fast-forwarding speed supported by DVR player.
+ */
+ public static final int MAX_FAST_FORWARD_SPEED = 256;
+
+ private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
+ private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826
+
+ private RecordedProgram mProgram;
+ private long mInitialSeekPositionMs;
+ private final TvView mTvView;
+ private DvrPlayerCallback mCallback;
+ private AspectRatioChangedListener mAspectRatioChangedListener;
+ private ContentBlockedListener mContentBlockedListener;
+ private float mAspectRatio = Float.NaN;
+ private int mPlaybackState = PlaybackState.STATE_NONE;
+ private long mTimeShiftCurrentPositionMs;
+ private boolean mPauseOnPrepared;
+ private final PlaybackParams mPlaybackParams = new PlaybackParams();
+ private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback();
+
+ public static class DvrPlayerCallback {
+ /**
+ * Called when the playback position is changed. The normal updating frequency is
+ * around 1 sec., which is restricted to the implementation of
+ * {@link android.media.tv.TvInputService}.
+ */
+ public void onPlaybackPositionChanged(long positionMs) { }
+ /**
+ * Called when the playback state or the playback speed is changed.
+ */
+ public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { }
+ }
+
+ public interface AspectRatioChangedListener {
+ /**
+ * Called when the Video's aspect ratio is changed.
+ */
+ void onAspectRatioChanged(float videoAspectRatio);
+ }
+
+ public interface ContentBlockedListener {
+ /**
+ * Called when the Video's aspect ratio is changed.
+ */
+ void onContentBlocked(TvContentRating rating);
+ }
+
+ public DvrPlayer(TvView tvView) {
+ mTvView = tvView;
+ mPlaybackParams.setSpeed(1.0f);
+ setTvViewCallbacks();
+ setCallback(null);
+ }
+
+ /**
+ * Prepares playback.
+ *
+ * @param doPlay indicates DVR player do or do not start playback after media is prepared.
+ */
+ public void prepare(boolean doPlay) throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "prepare()");
+ if (mProgram == null) {
+ throw new IllegalStateException("Recorded program not set");
+ } else if (mPlaybackState != PlaybackState.STATE_NONE) {
+ throw new IllegalStateException("Playback is already prepared");
+ }
+ mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri());
+ mPlaybackState = PlaybackState.STATE_CONNECTING;
+ mPauseOnPrepared = !doPlay;
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
+
+ /**
+ * Resumes playback.
+ */
+ public void play() throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "play()");
+ if (!isPlaybackPrepared()) {
+ throw new IllegalStateException("Recorded program not set or video not ready yet");
+ }
+ switch (mPlaybackState) {
+ case PlaybackState.STATE_FAST_FORWARDING:
+ case PlaybackState.STATE_REWINDING:
+ setPlaybackSpeed(1);
+ break;
+ default:
+ mTvView.timeShiftResume();
+ }
+ mPlaybackState = PlaybackState.STATE_PLAYING;
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
+
+ /**
+ * Pauses playback.
+ */
+ public void pause() throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "pause()");
+ if (!isPlaybackPrepared()) {
+ throw new IllegalStateException("Recorded program not set or playback not started yet");
+ }
+ switch (mPlaybackState) {
+ case PlaybackState.STATE_FAST_FORWARDING:
+ case PlaybackState.STATE_REWINDING:
+ setPlaybackSpeed(1);
+ // falls through
+ case PlaybackState.STATE_PLAYING:
+ mTvView.timeShiftPause();
+ mPlaybackState = PlaybackState.STATE_PAUSED;
+ break;
+ default:
+ break;
+ }
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
+
+ /**
+ * Fast-forwards playback with the given speed. If the given speed is larger than
+ * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}.
+ */
+ public void fastForward(int speed) throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "fastForward()");
+ if (!isPlaybackPrepared()) {
+ throw new IllegalStateException("Recorded program not set or playback not started yet");
+ }
+ if (speed <= 0) {
+ throw new IllegalArgumentException("Speed cannot be negative or 0");
+ }
+ if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) {
+ return;
+ }
+ speed = Math.min(speed, MAX_FAST_FORWARD_SPEED);
+ if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed);
+ setPlaybackSpeed(speed);
+ mPlaybackState = PlaybackState.STATE_FAST_FORWARDING;
+ mCallback.onPlaybackStateChanged(mPlaybackState, speed);
+ }
+
+ /**
+ * Rewinds playback with the given speed. If the given speed is larger than
+ * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}.
+ */
+ public void rewind(int speed) throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "rewind()");
+ if (!isPlaybackPrepared()) {
+ throw new IllegalStateException("Recorded program not set or playback not started yet");
+ }
+ if (speed <= 0) {
+ throw new IllegalArgumentException("Speed cannot be negative or 0");
+ }
+ if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) {
+ return;
+ }
+ speed = Math.min(speed, MAX_REWIND_SPEED);
+ if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed);
+ setPlaybackSpeed(-speed);
+ mPlaybackState = PlaybackState.STATE_REWINDING;
+ mCallback.onPlaybackStateChanged(mPlaybackState, speed);
+ }
+
+ /**
+ * Seeks playback to the specified position.
+ */
+ public void seekTo(long positionMs) throws IllegalStateException {
+ if (DEBUG) Log.d(TAG, "seekTo()");
+ if (!isPlaybackPrepared()) {
+ throw new IllegalStateException("Recorded program not set or playback not started yet");
+ }
+ if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) {
+ return;
+ }
+ positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS);
+ if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs);
+ mTvView.timeShiftSeekTo(positionMs);
+ if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING ||
+ mPlaybackState == PlaybackState.STATE_REWINDING) {
+ mPlaybackState = PlaybackState.STATE_PLAYING;
+ mTvView.timeShiftResume();
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
+ }
+
+ /**
+ * Resets playback.
+ */
+ public void reset() {
+ if (DEBUG) Log.d(TAG, "reset()");
+ mTvView.reset();
+ mPlaybackState = PlaybackState.STATE_NONE;
+ mTimeShiftCurrentPositionMs = 0;
+ mPlaybackParams.setSpeed(1.0f);
+ mProgram = null;
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
+
+ /**
+ * Sets callbacks for playback.
+ */
+ public void setCallback(DvrPlayerCallback callback) {
+ if (callback != null) {
+ mCallback = callback;
+ } else {
+ mCallback = mEmptyCallback;
+ }
+ }
+
+ /**
+ * Sets listener to aspect ratio changing.
+ */
+ public void setAspectRatioChangedListener(AspectRatioChangedListener listener) {
+ mAspectRatioChangedListener = listener;
+ }
+
+ /**
+ * Sets listener to content blocking.
+ */
+ public void setContentBlockedListener(ContentBlockedListener listener) {
+ mContentBlockedListener = listener;
+ }
+
+ /**
+ * Sets recorded programs for playback. If the player is playing another program, stops it.
+ */
+ public void setProgram(RecordedProgram program, long initialSeekPositionMs) {
+ if (mProgram != null && mProgram.equals(program)) {
+ return;
+ }
+ if (mPlaybackState != PlaybackState.STATE_NONE) {
+ reset();
+ }
+ mInitialSeekPositionMs = initialSeekPositionMs;
+ mProgram = program;
+ }
+
+ /**
+ * Returns the recorded program now playing.
+ */
+ public RecordedProgram getProgram() {
+ return mProgram;
+ }
+
+ /**
+ * Returns the currrent playback posistion in msecs.
+ */
+ public long getPlaybackPosition() {
+ return mTimeShiftCurrentPositionMs;
+ }
+
+ /**
+ * Returns the playback speed currently used.
+ */
+ public int getPlaybackSpeed() {
+ return (int) mPlaybackParams.getSpeed();
+ }
+
+ /**
+ * Returns the playback state defined in {@link android.media.session.PlaybackState}.
+ */
+ public int getPlaybackState() {
+ return mPlaybackState;
+ }
+
+ /**
+ * Returns if playback of the recorded program is started.
+ */
+ public boolean isPlaybackPrepared() {
+ return mPlaybackState != PlaybackState.STATE_NONE
+ && mPlaybackState != PlaybackState.STATE_CONNECTING;
+ }
+
+ private void setPlaybackSpeed(int speed) {
+ mPlaybackParams.setSpeed(speed);
+ mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
+ }
+
+ private long getRealSeekPosition(long seekPositionMs, long endMarginMs) {
+ return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs));
+ }
+
+ private void setTvViewCallbacks() {
+ mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
+ @Override
+ public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
+ // Workaround solution for b/29994826:
+ // prevents rewinding and fast-forwarding over the ends.
+ 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 {
+ mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
+ mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
+ }
+ }
+ });
+ mTvView.setCallback(new TvView.TvInputCallback() {
+ @Override
+ public void onTimeShiftStatusChanged(String inputId, int status) {
+ if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged");
+ 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);
+ }
+ }
+
+ @Override
+ public void onTrackSelected(String inputId, int type, String trackId) {
+ if (trackId == null || type != TvTrackInfo.TYPE_VIDEO
+ || mAspectRatioChangedListener == null) {
+ return;
+ }
+ List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
+ if (trackInfos != null) {
+ for (TvTrackInfo trackInfo : trackInfos) {
+ if (trackInfo.getId().equals(trackId)) {
+ float videoAspectRatio = trackInfo.getVideoPixelAspectRatio()
+ * trackInfo.getVideoWidth() / trackInfo.getVideoHeight();
+ if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
+ if (!Float.isNaN(videoAspectRatio)
+ && mAspectRatio != videoAspectRatio) {
+ mAspectRatioChangedListener
+ .onAspectRatioChanged(videoAspectRatio);
+ mAspectRatio = videoAspectRatio;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onContentBlocked(String inputId, TvContentRating rating) {
+ if (mContentBlockedListener != null) {
+ mContentBlockedListener.onContentBlocked(rating);
+ }
+ }
+ });
+ }
+} \ 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 2f3abccf..39be7961 100644
--- a/src/com/android/tv/dvr/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/DvrRecordingService.java
@@ -20,7 +20,6 @@ import android.app.AlarmManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
-import android.os.Binder;
import android.os.HandlerThread;
import android.os.IBinder;
import android.support.annotation.Nullable;
@@ -29,10 +28,10 @@ import android.util.Log;
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.util.Clock;
import com.android.tv.util.RecurringRunner;
-import com.android.tv.common.SoftPreconditions;
/**
* DVR Scheduler service.
@@ -60,31 +59,19 @@ public class DvrRecordingService extends Service {
private final Clock mClock = Clock.SYSTEM;
private RecurringRunner mReaperRunner;
- private WritableDvrDataManager mDataManager;
-
- /**
- * Class for clients to access. Because we know this service always
- * runs in the same process as its clients, we don't need to deal with
- * IPC.
- */
- public class SchedulerBinder extends Binder {
- Scheduler getScheduler() {
- return mScheduler;
- }
- }
-
- private final IBinder mBinder = new SchedulerBinder();
private Scheduler mScheduler;
private HandlerThread mHandlerThread;
@Override
public void onCreate() {
+ TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate();
SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
ApplicationSingletons singletons = TvApplication.getSingletons(this);
- mDataManager = (WritableDvrDataManager) singletons.getDvrDataManager();
+ DvrManager dvrManager = singletons.getDvrManager();
+ WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager();
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// mScheduler may have been set for testing.
@@ -92,12 +79,13 @@ public class DvrRecordingService extends Service {
mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
mHandlerThread.start();
mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(),
- singletons.getDvrSessionManger(), mDataManager,
- singletons.getChannelDataManager(), this, mClock, alarmManager);
+ singletons.getInputSessionManager(), dataManager,
+ singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), this,
+ mClock, alarmManager);
+ mScheduler.start();
}
- mDataManager.addScheduledRecordingListener(mScheduler);
mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1),
- new ScheduledProgramReaper(mDataManager, mClock), null);
+ new ScheduledProgramReaper(dataManager, mClock), null);
mReaperRunner.start();
}
@@ -112,7 +100,7 @@ public class DvrRecordingService extends Service {
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
mReaperRunner.stop();
- mDataManager.removeScheduledRecordingListener(mScheduler);
+ mScheduler.stop();
mScheduler = null;
if (mHandlerThread != null) {
mHandlerThread.quit();
@@ -124,7 +112,7 @@ public class DvrRecordingService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
- return mBinder;
+ return null;
}
@VisibleForTesting
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
new file mode 100644
index 00000000..aa77c400
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -0,0 +1,717 @@
+/*
+ * Copyright (C) 2015 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.media.tv.TvInputInfo;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import android.util.Range;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class to manage the schedules.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+@MainThread
+public class DvrScheduleManager {
+ private static final String TAG = "DvrScheduleManager";
+
+ /**
+ * The default priority of scheduled recording.
+ */
+ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
+ /**
+ * The default priority of series recording.
+ */
+ public static final long DEFAULT_SERIES_PRIORITY = DEFAULT_PRIORITY >> 1;
+ // The new priority will have the offset from the existing one.
+ private static final long PRIORITY_OFFSET = 1024;
+
+ 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<>();
+
+ private boolean mInitialized;
+
+ private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>();
+ private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners =
+ new ArraySet<>();
+
+ public DvrScheduleManager(Context context) {
+ mContext = context;
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDataManager = (DvrDataManagerImpl) appSingletons.getDvrDataManager();
+ mChannelDataManager = appSingletons.getChannelDataManager();
+ if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
+ buildData();
+ } else {
+ mDataManager.addDvrScheduleLoadFinishedListener(
+ new OnDvrScheduleLoadFinishedListener() {
+ @Override
+ public void onDvrScheduleLoadFinished() {
+ mDataManager.removeDvrScheduleLoadFinishedListener(this);
+ if (mChannelDataManager.isDbLoadFinished() && !mInitialized) {
+ buildData();
+ }
+ }
+ });
+ }
+ ScheduledRecordingListener scheduledRecordingListener = new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ if (!mInitialized) {
+ return;
+ }
+ for (ScheduledRecording schedule : scheduledRecordings) {
+ if (!schedule.isNotStarted() && !schedule.isInProgress()) {
+ continue;
+ }
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext,
+ schedule.getChannelId());
+ if (input == null) {
+ // Input removed.
+ continue;
+ }
+ String inputId = input.getId();
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules == null) {
+ schedules = new ArrayList<>();
+ mInputScheduleMap.put(inputId, schedules);
+ }
+ schedules.add(schedule);
+ }
+ onSchedulesChanged();
+ notifyScheduledRecordingAdded(scheduledRecordings);
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ if (!mInitialized) {
+ return;
+ }
+ for (ScheduledRecording schedule : scheduledRecordings) {
+ TvInputInfo input = Utils
+ .getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ if (input == null) {
+ // Input removed.
+ continue;
+ }
+ String inputId = input.getId();
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules != null) {
+ schedules.remove(schedule);
+ if (schedules.isEmpty()) {
+ mInputScheduleMap.remove(inputId);
+ }
+ }
+ }
+ onSchedulesChanged();
+ notifyScheduledRecordingRemoved(scheduledRecordings);
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(
+ ScheduledRecording... scheduledRecordings) {
+ if (!mInitialized) {
+ return;
+ }
+ for (ScheduledRecording schedule : scheduledRecordings) {
+ TvInputInfo input = Utils
+ .getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ if (input == null) {
+ // Input removed.
+ continue;
+ }
+ String inputId = input.getId();
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules == null) {
+ schedules = new ArrayList<>();
+ mInputScheduleMap.put(inputId, schedules);
+ }
+ // Compare ID because ScheduledRecording.equals() doesn't work if the state
+ // is changed.
+ Iterator<ScheduledRecording> i = schedules.iterator();
+ while (i.hasNext()) {
+ if (i.next().getId() == schedule.getId()) {
+ i.remove();
+ break;
+ }
+ }
+ if (schedule.isNotStarted() || schedule.isInProgress()) {
+ schedules.add(schedule);
+ }
+ if (schedules.isEmpty()) {
+ mInputScheduleMap.remove(inputId);
+ }
+ }
+ onSchedulesChanged();
+ notifyScheduledRecordingStatusChanged(scheduledRecordings);
+ }
+ };
+ mDataManager.addScheduledRecordingListener(scheduledRecordingListener);
+ ChannelDataManager.Listener channelDataManagerListener = new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ if (mDataManager.isDvrScheduleLoadFinished() && !mInitialized) {
+ buildData();
+ }
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ if (mDataManager.isDvrScheduleLoadFinished()) {
+ buildData();
+ }
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ }
+ };
+ mChannelDataManager.addListener(channelDataManagerListener);
+ }
+
+ /**
+ * Returns the started recordings for the given input.
+ */
+ private List<ScheduledRecording> getStartedRecordings(String inputId) {
+ if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
+ return Collections.emptyList();
+ }
+ List<ScheduledRecording> result = new ArrayList<>();
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules != null) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ result.add(schedule);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void buildData() {
+ mInputScheduleMap.clear();
+ for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
+ if (!schedule.isNotStarted() && !schedule.isInProgress()) {
+ continue;
+ }
+ Channel channel = mChannelDataManager.getChannel(schedule.getChannelId());
+ if (channel != null) {
+ String inputId = channel.getInputId();
+ // Do not check whether the input is valid or not. The input might be temporarily
+ // invalid.
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules == null) {
+ schedules = new ArrayList<>();
+ mInputScheduleMap.put(inputId, schedules);
+ }
+ schedules.add(schedule);
+ }
+ }
+ mInitialized = true;
+ onSchedulesChanged();
+ }
+
+ private void onSchedulesChanged() {
+ List<ScheduledRecording> addedConflicts = new ArrayList<>();
+ List<ScheduledRecording> removedConflicts = new ArrayList<>();
+ for (String inputId : mInputScheduleMap.keySet()) {
+ List<ScheduledRecording> oldConflicts = mInputConflictMap.get(inputId);
+ Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>();
+ if (oldConflicts != null) {
+ for (ScheduledRecording r : oldConflicts) {
+ oldConflictMap.put(r.getId(), r);
+ }
+ }
+ List<ScheduledRecording> conflicts = getConflictingSchedules(inputId);
+ 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));
+ }
+ if (!addedConflicts.isEmpty()) {
+ notifyConflictStateChange(true, ScheduledRecording.toArray(addedConflicts));
+ }
+ }
+
+ /**
+ * Returns {@code true} if this class has been initialized.
+ */
+ public boolean isInitialized() {
+ return mInitialized;
+ }
+
+ /**
+ * Adds a {@link ScheduledRecordingListener}.
+ */
+ public final void addScheduledRecordingListener(ScheduledRecordingListener listener) {
+ mScheduledRecordingListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link ScheduledRecordingListener}.
+ */
+ public final void removeScheduledRecordingListener(ScheduledRecordingListener listener) {
+ mScheduledRecordingListeners.remove(listener);
+ }
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingAdded} for each listener.
+ */
+ private void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ l.onScheduledRecordingAdded(scheduledRecordings);
+ }
+ }
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingRemoved} for each listener.
+ */
+ private void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ l.onScheduledRecordingRemoved(scheduledRecordings);
+ }
+ }
+
+ /**
+ * Calls {@link ScheduledRecordingListener#onScheduledRecordingStatusChanged} for each listener.
+ */
+ private void notifyScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
+ l.onScheduledRecordingStatusChanged(scheduledRecordings);
+ }
+ }
+
+ /**
+ * Adds a {@link OnConflictStateChangeListener}.
+ */
+ public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
+ mOnConflictStateChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link OnConflictStateChangeListener}.
+ */
+ public final void removeOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
+ mOnConflictStateChangeListeners.remove(listener);
+ }
+
+ /**
+ * Calls {@link OnConflictStateChangeListener#onConflictStateChange} for each listener.
+ */
+ private void notifyConflictStateChange(boolean conflict,
+ ScheduledRecording... scheduledRecordings) {
+ for (OnConflictStateChangeListener l : mOnConflictStateChangeListeners) {
+ l.onConflictStateChange(conflict, scheduledRecordings);
+ }
+ }
+
+ /**
+ * Returns the priority for the program if it is recorded.
+ * <p>
+ * The recording will have the higher priority than the existing ones.
+ */
+ public long suggestNewPriority() {
+ if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
+ return DEFAULT_PRIORITY;
+ }
+ return suggestHighestPriority();
+ }
+
+ private long suggestHighestPriority() {
+ long highestPriority = DEFAULT_PRIORITY - PRIORITY_OFFSET;
+ for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
+ if (schedule.getPriority() > highestPriority) {
+ highestPriority = schedule.getPriority();
+ }
+ }
+ return highestPriority + PRIORITY_OFFSET;
+ }
+
+ /**
+ * Returns the priority for a series recording.
+ * <p>
+ * The recording will have the higher priority than the existing series.
+ */
+ public long suggestNewSeriesPriority() {
+ if (!SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet")) {
+ return DEFAULT_SERIES_PRIORITY;
+ }
+ return suggestHighestSeriesPriority();
+ }
+
+ /**
+ * Returns the priority for a series recording by order of series recording priority.
+ *
+ * Higher order will have higher priority.
+ */
+ public static long suggestSeriesPriority(int order) {
+ return DEFAULT_SERIES_PRIORITY + order * PRIORITY_OFFSET;
+ }
+
+ private long suggestHighestSeriesPriority() {
+ long highestPriority = DEFAULT_SERIES_PRIORITY - PRIORITY_OFFSET;
+ for (SeriesRecording schedule : mDataManager.getSeriesRecordings()) {
+ if (schedule.getPriority() > highestPriority) {
+ highestPriority = schedule.getPriority();
+ }
+ }
+ return highestPriority + PRIORITY_OFFSET;
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
+ * this program is.
+ * <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.
+ */
+ public List<ScheduledRecording> getConflictingSchedules(Program program) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ SoftPreconditions.checkState(Program.isValid(program), TAG,
+ "Program is invalid: " + program);
+ SoftPreconditions.checkState(
+ program.getStartTimeUtcMillis() < program.getEndTimeUtcMillis(), TAG,
+ "Program duration is empty: " + program);
+ if (!mInitialized || !Program.isValid(program)
+ || program.getStartTimeUtcMillis() >= program.getEndTimeUtcMillis()) {
+ return Collections.emptyList();
+ }
+ TvInputInfo input = Utils.getTvInputInfoForProgram(mContext, program);
+ if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(input, Collections.singletonList(
+ ScheduledRecording.builder(input.getId(), program)
+ .setPriority(suggestHighestPriority())
+ .build()));
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
+ * this channel is.
+ * <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.
+ */
+ public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs,
+ long endTimeMs) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
+ SoftPreconditions.checkState(startTimeMs < endTimeMs, TAG, "Recording duration is empty.");
+ if (!mInitialized || channelId == Channel.INVALID_ID || startTimeMs >= endTimeMs) {
+ return Collections.emptyList();
+ }
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
+ if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(input, Collections.singletonList(
+ ScheduledRecording.builder(input.getId(), channelId, startTimeMs, endTimeMs)
+ .setPriority(suggestHighestPriority())
+ .build()));
+ }
+
+ /**
+ * Returns all the scheduled recordings that conflicts and will not be recorded or clipped for
+ * the given input.
+ */
+ @NonNull
+ private List<ScheduledRecording> getConflictingSchedules(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();
+ }
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
+ if (schedules == null || schedules.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(schedules, input.getTunerCount());
+ }
+
+ /**
+ * Checks if the schedule is conflicting.
+ *
+ * <p>Note that the {@code schedule} should be the existing one. If not, this returns
+ * {@code false}.
+ */
+ public boolean isConflicting(ScheduledRecording schedule) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ 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);
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
+ * this channel is tuned to.
+ */
+ public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
+ SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: "
+ + channelId);
+ if (!mInitialized || channelId == Channel.INVALID_ID || input == null) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedulesForTune(input.getId(), channelId, System.currentTimeMillis(),
+ suggestHighestPriority(), getStartedRecordings(input.getId()),
+ input.getTunerCount());
+ }
+
+ @VisibleForTesting
+ public static List<ScheduledRecording> getConflictingSchedulesForTune(String inputId,
+ long channelId, long currentTimeMs, long newPriority,
+ List<ScheduledRecording> startedRecordings, int tunerCount) {
+ boolean channelFound = false;
+ for (ScheduledRecording schedule : startedRecordings) {
+ if (schedule.getChannelId() == channelId) {
+ channelFound = true;
+ break;
+ }
+ }
+ List<ScheduledRecording> schedules;
+ if (!channelFound) {
+ // The current channel is not being recorded.
+ schedules = new ArrayList<>(startedRecordings);
+ schedules.add(ScheduledRecording
+ .builder(inputId, channelId, currentTimeMs, currentTimeMs + 1)
+ .setPriority(newPriority)
+ .build());
+ } else {
+ schedules = startedRecordings;
+ }
+ return getConflictingSchedules(schedules, tunerCount);
+ }
+
+ /**
+ * Returns priority ordered list of all scheduled recordings that will not be recorded if
+ * the user keeps watching this channel.
+ * <p>
+ * Note that if the user keeps watching the channel, the channel can be recorded.
+ */
+ public List<ScheduledRecording> getConflictingSchedulesForWatching(long channelId) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ SoftPreconditions.checkState(channelId != Channel.INVALID_ID, TAG, "Invalid channel ID");
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, channelId);
+ SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID: "
+ + channelId);
+ if (!mInitialized || channelId == Channel.INVALID_ID || input == null) {
+ return Collections.emptyList();
+ }
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
+ if (schedules == null || schedules.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedulesForWatching(input.getId(), channelId,
+ System.currentTimeMillis(), suggestNewPriority(), schedules, input.getTunerCount());
+ }
+
+ private List<ScheduledRecording> getConflictingSchedules(TvInputInfo input,
+ List<ScheduledRecording> schedulesToAdd) {
+ SoftPreconditions.checkNotNull(input);
+ if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
+ return Collections.emptyList();
+ }
+ List<ScheduledRecording> currentSchedules = mInputScheduleMap.get(input.getId());
+ if (currentSchedules == null || currentSchedules.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(schedulesToAdd, currentSchedules, input.getTunerCount());
+ }
+
+ @VisibleForTesting
+ static List<ScheduledRecording> getConflictingSchedulesForWatching(String inputId,
+ long channelId, long currentTimeMs, long newPriority,
+ @NonNull List<ScheduledRecording> schedules, int tunerCount) {
+ List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
+ List<ScheduledRecording> schedulesSameChannel = new ArrayList<>();
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getChannelId() == channelId) {
+ schedulesSameChannel.add(schedule);
+ schedulesToCheck.remove(schedule);
+ }
+ }
+ // Assume that the user will watch the current channel forever.
+ schedulesToCheck.add(ScheduledRecording
+ .builder(inputId, channelId, currentTimeMs, Long.MAX_VALUE)
+ .setPriority(newPriority)
+ .build());
+ List<ScheduledRecording> result = new ArrayList<>();
+ result.addAll(getConflictingSchedules(schedulesSameChannel, 1));
+ result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount));
+ Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR);
+ return result;
+ }
+
+ @VisibleForTesting
+ static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedulesToAdd,
+ List<ScheduledRecording> currentSchedules, int tunerCount) {
+ List<ScheduledRecording> schedulesToCheck = new ArrayList<>(currentSchedules);
+ // When the duplicate schedule is to be added, remove the current duplicate recording.
+ for (Iterator<ScheduledRecording> iter = schedulesToCheck.iterator(); iter.hasNext(); ) {
+ ScheduledRecording schedule = iter.next();
+ for (ScheduledRecording toAdd : schedulesToAdd) {
+ if (schedule.getType() == ScheduledRecording.TYPE_PROGRAM) {
+ if (toAdd.getProgramId() == schedule.getProgramId()) {
+ iter.remove();
+ break;
+ }
+ } else {
+ if (toAdd.getChannelId() == schedule.getChannelId()
+ && toAdd.getStartTimeMs() == schedule.getStartTimeMs()
+ && toAdd.getEndTimeMs() == schedule.getEndTimeMs()) {
+ iter.remove();
+ break;
+ }
+ }
+ }
+ }
+ schedulesToCheck.addAll(schedulesToAdd);
+ List<Range<Long>> ranges = new ArrayList<>();
+ for (ScheduledRecording schedule : schedulesToAdd) {
+ ranges.add(new Range<>(schedule.getStartTimeMs(), schedule.getEndTimeMs()));
+ }
+ return getConflictingSchedules(schedulesToCheck, tunerCount, ranges);
+ }
+
+ /**
+ * Returns all conflicting scheduled recordings for the given schedules and count of tuner.
+ */
+ public static List<ScheduledRecording> getConflictingSchedules(
+ List<ScheduledRecording> schedules, int tunerCount) {
+ return getConflictingSchedules(schedules, tunerCount,
+ Collections.singletonList(new Range<>(Long.MIN_VALUE, Long.MAX_VALUE)));
+ }
+
+ @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;
+ }
+ }
+ }
+ // 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);
+ }
+ }
+ 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(); ) {
+ ScheduledRecording schedule = iter.next();
+ if (channelIds.contains(schedule.getChannelId())) {
+ conflicts.add(schedule);
+ iter.remove();
+ } else {
+ channelIds.add(schedule.getChannelId());
+ }
+ }
+ if (overlaps.size() > tunerCount) {
+ conflicts.addAll(overlaps.subList(tunerCount, overlaps.size()));
+ }
+ }
+ List<ScheduledRecording> result = new ArrayList<>(conflicts);
+ Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR);
+ return result;
+ }
+
+ /**
+ * A listener which is notified the conflict state change of the schedules.
+ */
+ public interface OnConflictStateChangeListener {
+ /**
+ * Called when the conflicting schedules change.
+ *
+ * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise
+ * {@code false}.
+ * @param schedules the schedules
+ */
+ void onConflictStateChange(boolean conflict, ScheduledRecording... schedules);
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrSessionManager.java b/src/com/android/tv/dvr/DvrSessionManager.java
deleted file mode 100644
index fba05cb6..00000000
--- a/src/com/android/tv/dvr/DvrSessionManager.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2015 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.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
-import android.media.tv.TvRecordingClient;
-import android.os.Build;
-import android.os.Handler;
-import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.util.ArrayMap;
-import android.util.Log;
-
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.data.Channel;
-
-/**
- * Manages Dvr Sessions.
- * Responsible for:
- * <ul>
- * <li>Manage DvrSession</li>
- * <li>Manage capabilities (conflict)</li>
- * </ul>
- */
-@TargetApi(Build.VERSION_CODES.N)
-public class DvrSessionManager extends TvInputManager.TvInputCallback {
- //consider moving all of this to TvInputManagerHelper
- private final static String TAG = "DvrSessionManager";
- private static final boolean DEBUG = false;
-
- private final Context mContext;
- private final TvInputManager mTvInputManager;
- private final ArrayMap<String, TvInputInfo> mRecordingTvInputs = new ArrayMap<>();
-
- public DvrSessionManager(Context context) {
- this(context, (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE),
- new Handler());
- }
-
- @VisibleForTesting
- DvrSessionManager(Context context, TvInputManager tvInputManager, Handler handler) {
- SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
- mTvInputManager = tvInputManager;
- mContext = context.getApplicationContext();
- for (TvInputInfo info : tvInputManager.getTvInputList()) {
- if (DEBUG) {
- Log.d(TAG, info + " canRecord=" + info.canRecord() + " tunerCount=" + info
- .getTunerCount());
- }
- if (info.canRecord()) {
- mRecordingTvInputs.put(info.getId(), info);
- }
- }
- tvInputManager.registerCallback(this, handler);
-
- }
-
- public TvRecordingClient createTvRecordingClient(String tag,
- TvRecordingClient.RecordingCallback callback, Handler handler) {
- return new TvRecordingClient(mContext, tag, callback, handler);
- }
-
- public boolean canAcquireDvrSession(String inputId, Channel channel) {
- // TODO(DVR): implement checking tuner count etc.
- TvInputInfo info = mRecordingTvInputs.get(inputId);
- return info != null;
- }
-
- public void releaseTvRecordingClient(TvRecordingClient recordingClient) {
- recordingClient.release();
- }
-
- @Override
- public void onInputAdded(String inputId) {
- super.onInputAdded(inputId);
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
- if (DEBUG) {
- Log.d(TAG, "onInputAdded " + info.toString() + " canRecord=" + info.canRecord()
- + " tunerCount=" + info.getTunerCount());
- }
- if (info.canRecord()) {
- mRecordingTvInputs.put(inputId, info);
- }
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- super.onInputRemoved(inputId);
- if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
- mRecordingTvInputs.remove(inputId);
- }
-
- @Override
- public void onInputUpdated(String inputId) {
- super.onInputUpdated(inputId);
- TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
- if (DEBUG) {
- Log.d(TAG, "onInputUpdated " + info.toString() + " canRecord=" + info.canRecord()
- + " tunerCount=" + info.getTunerCount());
- }
- if (info.canRecord()) {
- mRecordingTvInputs.put(inputId, info);
- } else {
- mRecordingTvInputs.remove(inputId);
- }
- }
-
- @Nullable
- public TvInputInfo getTvInputInfo(String inputId) {
- return mRecordingTvInputs.get(inputId);
- }
-}
diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java
index 3649ad1e..6d2f0d43 100644
--- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java
+++ b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java
@@ -16,6 +16,8 @@
package com.android.tv.dvr;
+import com.android.tv.TvApplication;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -26,6 +28,7 @@ import android.content.Intent;
public class DvrStartRecordingReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ TvApplication.setCurrentRunningProcess(context, true);
DvrRecordingService.startService(context);
}
}
diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java
new file mode 100644
index 00000000..be934fd4
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrUiHelper.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2015 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.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.tv.TvInputManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ActivityOptionsCompat;
+import android.text.TextUtils;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.tv.MainActivity;
+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.ui.DvrCancelAllSeriesRecordingDialogFragment;
+import com.android.tv.dvr.ui.DvrDetailsActivity;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
+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.DvrSeriesSettingsActivity;
+import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
+import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+import com.android.tv.util.Utils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A helper class for DVR UI.
+ */
+@MainThread
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrUiHelper {
+ /**
+ * Handles the action to create the new schedule. It returns {@code true} if the schedule is
+ * added and there's no additional UI, otherwise {@code false}.
+ */
+ public static boolean handleCreateSchedule(MainActivity activity, Program program) {
+ if (program == null) {
+ return false;
+ }
+ DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
+ if (!program.isEpisodic()) {
+ // One time recording.
+ dvrManager.addSchedule(program);
+ if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
+ DvrUiHelper.showScheduleConflictDialog(activity, program);
+ return false;
+ }
+ } else {
+ SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
+ if (seriesRecording == null) {
+ DvrUiHelper.showScheduleDialog(activity, program);
+ return false;
+ } else {
+ // Show recorded program rather than the schedule.
+ RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (recordedProgram != null) {
+ DvrUiHelper.showAlreadyRecordedDialog(activity, program);
+ return false;
+ }
+ ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (duplicate != null
+ && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || duplicate.getState()
+ == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ DvrUiHelper.showAlreadyScheduleDialog(activity, program);
+ return false;
+ }
+ // Just add the schedule.
+ dvrManager.addSchedule(program);
+ }
+ }
+ return true;
+
+ }
+
+ /**
+ * Shows the schedule dialog.
+ */
+ public static void showScheduleDialog(MainActivity activity, Program program) {
+ if (SoftPreconditions.checkNotNull(program) == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true);
+ }
+
+ /**
+ * Shows the recording duration options dialog.
+ */
+ public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) {
+ if (SoftPreconditions.checkNotNull(channel) == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
+ showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args);
+ }
+
+ /**
+ * Shows the dialog which says that the new schedule conflicts with others.
+ */
+ public static void showScheduleConflictDialog(MainActivity activity, Program program) {
+ if (program == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true);
+ }
+
+ /**
+ * Shows the conflict dialog for the channel watching.
+ */
+ public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) {
+ if (channel == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
+ showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args);
+ }
+
+ /**
+ * Shows DVR insufficient space error dialog.
+ */
+ public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) {
+ showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null);
+ Utils.clearRecordingFailedReason(activity,
+ TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ }
+
+ /**
+ * Shows DVR missing storage error dialog.
+ */
+ public static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) {
+ SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId));
+ Bundle args = new Bundle();
+ args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId);
+ showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args);
+ }
+
+ /**
+ * Shows stop recording dialog.
+ */
+ public static void showStopRecordingDialog(MainActivity activity, Channel channel) {
+ if (channel == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
+ showDialogFragment(activity, new DvrStopRecordingDialogFragment(), args);
+ }
+
+ /**
+ * Shows "already scheduled" dialog.
+ */
+ public static void showAlreadyScheduleDialog(MainActivity activity, Program program) {
+ if (program == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true);
+ }
+
+ /**
+ * Shows "already recorded" dialog.
+ */
+ public static void showAlreadyRecordedDialog(MainActivity activity, Program program) {
+ if (program == null) {
+ return;
+ }
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true);
+ }
+
+ private static void showDialogFragment(Activity activity,
+ DvrHalfSizedDialogFragment dialogFragment, Bundle args) {
+ showDialogFragment(activity, dialogFragment, args, false, false);
+ }
+
+ private static void showDialogFragment(Activity activity,
+ DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory,
+ boolean keepProgramGuide) {
+ dialogFragment.setArguments(args);
+ if (activity instanceof MainActivity) {
+ ((MainActivity) activity).getOverlayManager()
+ .showDialogFragment(DvrHalfSizedDialogFragment.DIALOG_TAG, dialogFragment,
+ keepSidePanelHistory, keepProgramGuide);
+ } else {
+ dialogFragment.show(activity.getFragmentManager(),
+ DvrHalfSizedDialogFragment.DIALOG_TAG);
+ }
+ }
+
+ /**
+ * Checks whether channel watch conflict dialog is open or not.
+ */
+ public static boolean isChannelWatchConflictDialogShown(MainActivity activity) {
+ return activity.getOverlayManager().getCurrentDialog() instanceof
+ DvrChannelWatchConflictDialogFragment;
+ }
+
+ private static ScheduledRecording getEarliestScheduledRecording(List<ScheduledRecording>
+ recordings) {
+ ScheduledRecording earlistScheduledRecording = null;
+ if (!recordings.isEmpty()) {
+ Collections.sort(recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ earlistScheduledRecording = recordings.get(0);
+ }
+ return earlistScheduledRecording;
+ }
+
+ /**
+ * Shows the schedules activity to resolve the tune conflict.
+ */
+ public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) {
+ if (channel == null) {
+ return;
+ }
+ List<ScheduledRecording> conflicts = TvApplication.getSingletons(context).getDvrManager()
+ .getConflictingSchedulesForTune(channel.getId());
+ startSchedulesActivity(context, getEarliestScheduledRecording(conflicts));
+ }
+
+ /**
+ * Shows the schedules activity to resolve the one time recording conflict.
+ */
+ public static void startSchedulesActivityForOneTimeRecordingConflict(Context context,
+ List<ScheduledRecording> conflicts) {
+ startSchedulesActivity(context, getEarliestScheduledRecording(conflicts));
+ }
+
+ /**
+ * Shows the schedules activity with full schedule.
+ */
+ public static void startSchedulesActivity(Context context, ScheduledRecording
+ focusedScheduledRecording) {
+ Intent intent = new Intent(context, DvrSchedulesActivity.class);
+ intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE,
+ DvrSchedulesActivity.TYPE_FULL_SCHEDULE);
+ if (focusedScheduledRecording != null) {
+ intent.putExtra(DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING,
+ focusedScheduledRecording);
+ }
+ context.startActivity(intent);
+ }
+
+ /**
+ * Shows the schedules activity for series recording.
+ */
+ public static void startSchedulesActivityForSeries(Context context,
+ SeriesRecording seriesRecording) {
+ Intent intent = new Intent(context, DvrSchedulesActivity.class);
+ intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE,
+ DvrSchedulesActivity.TYPE_SERIES_SCHEDULE);
+ intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING,
+ seriesRecording);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Shows the series settings activity.
+ */
+ public static void startSeriesSettingsActivity(Context context, long seriesRecordingId) {
+ Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
+ intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Shows the details activity for the schedule.
+ */
+ public static void startDetailsActivity(Activity activity, ScheduledRecording schedule,
+ @Nullable ImageView imageView, boolean hideViewSchedule) {
+ if (schedule == null) {
+ return;
+ }
+ 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;
+ } else {
+ return;
+ }
+ Intent intent = new Intent(activity, DvrDetailsActivity.class);
+ 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) {
+ bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView,
+ DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
+ }
+ activity.startActivity(intent, bundle);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Shows the series deletion activity.
+ */
+ public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) {
+ Intent intent = new Intent(context, DvrSeriesDeletionActivity.class);
+ intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId);
+ context.startActivity(intent);
+ }
+
+ public static void showAddScheduleToast(Context context,
+ String title, long startTimeMs, long endTimeMs) {
+ String msg = (startTimeMs > System.currentTimeMillis()) ?
+ context.getString(R.string.dvr_msg_program_scheduled, title)
+ : context.getString(R.string.dvr_msg_current_program_scheduled, title,
+ Utils.toTimeString(endTimeMs, false));
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
new file mode 100644
index 00000000..cb723f83
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
@@ -0,0 +1,119 @@
+/*
+ * 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.Context;
+import android.content.SharedPreferences;
+import android.media.tv.TvInputManager;
+
+import com.android.tv.common.SharedPreferencesUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * A class to manage DVR watched state.
+ * It will remember and provides previous watched position of DVR playback.
+ */
+public class DvrWatchedPositionManager {
+ private final static String TAG = "DvrWatchedPositionManager";
+ private final boolean DEBUG = false;
+
+ private SharedPreferences mWatchedPositions;
+ private final Context mContext;
+ private final Map<Long, Set> mListeners = new HashMap<>();
+
+ public DvrWatchedPositionManager(Context context) {
+ mContext = context.getApplicationContext();
+ mWatchedPositions = mContext.getSharedPreferences(SharedPreferencesUtils
+ .SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Sets the watched position of the give program.
+ */
+ public void setWatchedPosition(long recordedProgramId, long positionMs) {
+ mWatchedPositions.edit().putLong(Long.toString(recordedProgramId), positionMs).apply();
+ notifyWatchedPositionChanged(recordedProgramId, positionMs);
+ }
+
+ /**
+ * Gets the watched position of the give program.
+ */
+ public long getWatchedPosition(long recordedProgramId) {
+ return mWatchedPositions.getLong(Long.toString(recordedProgramId),
+ TvInputManager.TIME_SHIFT_INVALID_TIME);
+ }
+
+ /**
+ * Adds {@link WatchedPositionChangedListener}.
+ */
+ public void addListener(WatchedPositionChangedListener listener, long recordedProgramId) {
+ if (recordedProgramId == RecordedProgram.ID_NOT_SET) {
+ return;
+ }
+ Set<WatchedPositionChangedListener> listenerSet = mListeners.get(recordedProgramId);
+ if (listenerSet == null) {
+ listenerSet = new CopyOnWriteArraySet<>();
+ mListeners.put(recordedProgramId, listenerSet);
+ }
+ listenerSet.add(listener);
+ }
+
+ /**
+ * Removes {@link WatchedPositionChangedListener}.
+ */
+ public void removeListener(WatchedPositionChangedListener listener) {
+ for (long recordedProgramId : new ArrayList<>(mListeners.keySet())) {
+ removeListener(listener, recordedProgramId);
+ }
+ }
+
+ /**
+ * Removes {@link WatchedPositionChangedListener}.
+ */
+ public void removeListener(WatchedPositionChangedListener listener, long recordedProgramId) {
+ Set<WatchedPositionChangedListener> listenerSet = mListeners.get(recordedProgramId);
+ if (listenerSet == null) {
+ return;
+ }
+ listenerSet.remove(listener);
+ if (listenerSet.isEmpty()) {
+ mListeners.remove(recordedProgramId);
+ }
+ }
+
+ private void notifyWatchedPositionChanged(long recordedProgramId, long positionMs) {
+ Set<WatchedPositionChangedListener> listenerSet = mListeners.get(recordedProgramId);
+ if (listenerSet == null) {
+ return;
+ }
+ for (WatchedPositionChangedListener listener : listenerSet) {
+ listener.onWatchedPositionChanged(recordedProgramId, positionMs);
+ }
+ }
+
+ public interface WatchedPositionChangedListener {
+ /**
+ * Called when the watched position of some program is changed.
+ */
+ void onWatchedPositionChanged(long recordedProgramId, long positionMs);
+ }
+}
diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/IdGenerator.java
new file mode 100644
index 00000000..0ed6362c
--- /dev/null
+++ b/src/com/android/tv/dvr/IdGenerator.java
@@ -0,0 +1,50 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A class which generate the ID which increases sequentially.
+ */
+public class IdGenerator {
+ /**
+ * ID generator for the scheduled recording.
+ */
+ public static final IdGenerator SCHEDULED_RECORDING = new IdGenerator();
+
+ /**
+ * ID generator for the series recording.
+ */
+ public static final IdGenerator SERIES_RECORDING = new IdGenerator();
+
+ private final AtomicLong mMaxId = new AtomicLong(0);
+
+ /**
+ * Sets the new maximum ID.
+ */
+ public void setMaxId(long maxId) {
+ mMaxId.set(maxId);
+ }
+
+ /**
+ * Returns the new ID which is greater than the existing maximum ID by 1.
+ */
+ public long newId() {
+ return mMaxId.incrementAndGet();
+ }
+}
diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java
new file mode 100644
index 00000000..23eacb73
--- /dev/null
+++ b/src/com/android/tv/dvr/InputTaskScheduler.java
@@ -0,0 +1,378 @@
+/*
+ * 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.Context;
+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;
+import android.util.Log;
+import android.util.LongSparseArray;
+
+import com.android.tv.InputSessionManager;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.util.Clock;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+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;
+
+ private static final int MSG_ADD_SCHEDULED_RECORDING = 1;
+ 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;
+
+ /**
+ * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
+ */
+ public final class HandlerWrapper extends Handler {
+ public static final int MESSAGE_REMOVE = 999;
+ private final long mId;
+ private final RecordingTask mTask;
+
+ HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording,
+ RecordingTask recordingTask) {
+ super(looper, recordingTask);
+ mId = scheduledRecording.getId();
+ mTask = recordingTask;
+ mTask.setHandler(this);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ // The RecordingTask gets a chance first.
+ // It must return false to pass this message to here.
+ if (msg.what == MESSAGE_REMOVE) {
+ if (DEBUG) Log.d(TAG, "done " + mId);
+ mPendingRecordings.remove(mId);
+ }
+ removeCallbacksAndMessages(null);
+ mHandler.removeMessages(MSG_BUILD_SCHEDULE);
+ mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
+ super.handleMessage(msg);
+ }
+ }
+
+ private TvInputInfo mInput;
+ private final Looper mLooper;
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrManager mDvrManager;
+ private final WritableDvrDataManager mDataManager;
+ private final InputSessionManager mSessionManager;
+ private final Clock mClock;
+ private final Context mContext;
+
+ private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>();
+ private final Map<Long, ScheduledRecording> mWaitingSchedules = new ArrayMap<>();
+ private final Handler mMainThreadHandler;
+ private final Handler mHandler;
+ private final Object mInputLock = new Object();
+ private final RecordingTaskFactory mRecordingTaskFactory;
+
+ public InputTaskScheduler(Context context, TvInputInfo input, Looper looper,
+ ChannelDataManager channelDataManager, DvrManager dvrManager,
+ DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) {
+ this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager,
+ clock, new Handler(Looper.getMainLooper()), null, null);
+ }
+
+ @VisibleForTesting
+ InputTaskScheduler(Context context, TvInputInfo input, Looper looper,
+ ChannelDataManager channelDataManager, DvrManager dvrManager,
+ DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock,
+ Handler mainThreadHandler, @Nullable Handler workerThreadHandler,
+ RecordingTaskFactory recordingTaskFactory) {
+ if (DEBUG) Log.d(TAG, "Creating scheduler for " + input);
+ mContext = context;
+ mInput = input;
+ mLooper = looper;
+ mChannelDataManager = channelDataManager;
+ mDvrManager = dvrManager;
+ mDataManager = (WritableDvrDataManager) dataManager;
+ mSessionManager = sessionManager;
+ mClock = clock;
+ mMainThreadHandler = mainThreadHandler;
+ mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory
+ : new RecordingTaskFactory() {
+ @Override
+ public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel,
+ DvrManager dvrManager, InputSessionManager sessionManager,
+ WritableDvrDataManager dataManager, Clock clock) {
+ return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager,
+ mDataManager, mClock);
+ }
+ };
+ if (workerThreadHandler == null) {
+ mHandler = new WorkerThreadHandler(looper);
+ } else {
+ mHandler = workerThreadHandler;
+ }
+ }
+
+ /**
+ * Adds a {@link ScheduledRecording}.
+ */
+ public void addSchedule(ScheduledRecording schedule) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule));
+ }
+
+ @VisibleForTesting
+ void handleAddSchedule(ScheduledRecording schedule) {
+ if (mPendingRecordings.get(schedule.getId()) != null
+ || mWaitingSchedules.containsKey(schedule.getId())) {
+ return;
+ }
+ mWaitingSchedules.put(schedule.getId(), schedule);
+ mHandler.removeMessages(MSG_BUILD_SCHEDULE);
+ mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
+ }
+
+ /**
+ * Removes the {@link ScheduledRecording}.
+ */
+ public void removeSchedule(ScheduledRecording schedule) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule));
+ }
+
+ @VisibleForTesting
+ void handleRemoveSchedule(ScheduledRecording schedule) {
+ HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
+ if (wrapper != null) {
+ wrapper.mTask.cancel();
+ return;
+ }
+ if (mWaitingSchedules.containsKey(schedule.getId())) {
+ mWaitingSchedules.remove(schedule.getId());
+ mHandler.removeMessages(MSG_BUILD_SCHEDULE);
+ mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
+ }
+ }
+
+ /**
+ * Updates the {@link ScheduledRecording}.
+ */
+ public void updateSchedule(ScheduledRecording schedule) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule));
+ }
+
+ @VisibleForTesting
+ void handleUpdateSchedule(ScheduledRecording schedule) {
+ HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId());
+ if (wrapper != null) {
+ if (schedule.getStartTimeMs() > mClock.currentTimeMillis()
+ && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) {
+ // It shouldn't have started. Cancel and put to the waiting list.
+ // The schedules will be rebuilt when the task is removed.
+ // The reschedule is called in Scheduler.
+ wrapper.mTask.cancel();
+ mWaitingSchedules.put(schedule.getId(), schedule);
+ return;
+ }
+ wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule));
+ return;
+ }
+ if (mWaitingSchedules.containsKey(schedule.getId())) {
+ mWaitingSchedules.put(schedule.getId(), schedule);
+ mHandler.removeMessages(MSG_BUILD_SCHEDULE);
+ mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE);
+ }
+ }
+
+ /**
+ * Updates the TV input.
+ */
+ public void updateTvInputInfo(TvInputInfo input) {
+ synchronized (mInputLock) {
+ mInput = input;
+ }
+ }
+
+ @VisibleForTesting
+ void handleBuildSchedule() {
+ if (mWaitingSchedules.isEmpty()) {
+ return;
+ }
+ long currentTimeMs = mClock.currentTimeMillis();
+ // Remove past schedules.
+ for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator();
+ iter.hasNext(); ) {
+ ScheduledRecording schedule = iter.next();
+ if (schedule.getEndTimeMs() <= currentTimeMs) {
+ fail(schedule);
+ iter.remove();
+ }
+ }
+ if (mWaitingSchedules.isEmpty()) {
+ return;
+ }
+ // Record the schedules which should start now.
+ List<ScheduledRecording> schedulesToStart = new ArrayList<>();
+ for (ScheduledRecording schedule : mWaitingSchedules.values()) {
+ if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED
+ && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS
+ <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) {
+ schedulesToStart.add(schedule);
+ }
+ }
+ Collections.sort(schedulesToStart, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ int tunerCount;
+ synchronized (mInputLock) {
+ tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;
+ }
+ for (ScheduledRecording schedule : schedulesToStart) {
+ if (hasTaskWhichFinishEarlier(schedule)) {
+ // If there is a schedule which finishes earlier than the new schedule, rebuild the
+ // schedules after it finishes.
+ return;
+ }
+ if (mPendingRecordings.size() < tunerCount) {
+ // Tuners available.
+ createRecordingTask(schedule).start();
+ mWaitingSchedules.remove(schedule.getId());
+ } else {
+ // No available tuners.
+ RecordingTask task = getReplacableTask(schedule);
+ if (task != null) {
+ 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());
+ }
+ }
+ }
+ if (mWaitingSchedules.isEmpty()) {
+ return;
+ }
+ // Set next scheduling.
+ long earliest = Long.MAX_VALUE;
+ for (ScheduledRecording schedule : mWaitingSchedules.values()) {
+ if (earliest > schedule.getStartTimeMs()) {
+ earliest = schedule.getStartTimeMs();
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest
+ - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - currentTimeMs);
+ }
+
+ private RecordingTask createRecordingTask(ScheduledRecording schedule) {
+ Channel channel = mChannelDataManager.getChannel(schedule.getChannelId());
+ RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel,
+ mDvrManager, mSessionManager, mDataManager, mClock);
+ HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask);
+ mPendingRecordings.put(schedule.getId(), handlerWrapper);
+ return recordingTask;
+ }
+
+ private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) {
+ int size = mPendingRecordings.size();
+ for (int i = 0; i < size; ++i) {
+ RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
+ if (task.getEndTimeMs() <= schedule.getStartTimeMs()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private RecordingTask getReplacableTask(ScheduledRecording schedule) {
+ 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;
+ }
+ }
+ return candidate;
+ }
+
+ private void fail(ScheduledRecording schedule) {
+ // It's called when the scheduling has been failed without creating RecordingTask.
+ runOnMainHandler(new Runnable() {
+ @Override
+ public void run() {
+ ScheduledRecording scheduleInManager =
+ mDataManager.getScheduledRecording(schedule.getId());
+ if (scheduleInManager != null) {
+ // The schedule should be updated based on the object from DataManager in case
+ // when it has been updated.
+ mDataManager.changeState(scheduleInManager,
+ ScheduledRecording.STATE_RECORDING_FAILED);
+ }
+ }
+ });
+ }
+
+ private void runOnMainHandler(Runnable runnable) {
+ if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+ runnable.run();
+ } else {
+ mMainThreadHandler.post(runnable);
+ }
+ }
+
+ @VisibleForTesting
+ interface RecordingTaskFactory {
+ RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel,
+ DvrManager dvrManager, InputSessionManager sessionManager,
+ WritableDvrDataManager dataManager, Clock clock);
+ }
+
+ private class WorkerThreadHandler extends Handler {
+ public WorkerThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ADD_SCHEDULED_RECORDING:
+ handleAddSchedule((ScheduledRecording) msg.obj);
+ break;
+ case MSG_REMOVE_SCHEDULED_RECORDING:
+ handleRemoveSchedule((ScheduledRecording) msg.obj);
+ break;
+ case MSG_UPDATE_SCHEDULED_RECORDING:
+ handleUpdateSchedule((ScheduledRecording) msg.obj);
+ case MSG_BUILD_SCHEDULE:
+ handleBuildSchedule();
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java
new file mode 100644
index 00000000..085402a4
--- /dev/null
+++ b/src/com/android/tv/dvr/RecordedProgram.java
@@ -0,0 +1,825 @@
+/*
+ * 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 static android.media.tv.TvContract.RecordedPrograms;
+
+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.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.common.R;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.InternalDataUtils;
+import com.android.tv.util.Utils;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}.
+ */
+public class RecordedProgram extends BaseProgram {
+ public static final int ID_NOT_SET = -1;
+
+ public final static String[] PROJECTION = {
+ // These are in exactly the order listed in RecordedPrograms
+ RecordedPrograms._ID,
+ RecordedPrograms.COLUMN_PACKAGE_NAME,
+ RecordedPrograms.COLUMN_INPUT_ID,
+ RecordedPrograms.COLUMN_CHANNEL_ID,
+ RecordedPrograms.COLUMN_TITLE,
+ RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
+ RecordedPrograms.COLUMN_SEASON_TITLE,
+ RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
+ RecordedPrograms.COLUMN_EPISODE_TITLE,
+ RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+ RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
+ RecordedPrograms.COLUMN_BROADCAST_GENRE,
+ RecordedPrograms.COLUMN_CANONICAL_GENRE,
+ RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
+ RecordedPrograms.COLUMN_LONG_DESCRIPTION,
+ RecordedPrograms.COLUMN_VIDEO_WIDTH,
+ RecordedPrograms.COLUMN_VIDEO_HEIGHT,
+ RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
+ RecordedPrograms.COLUMN_CONTENT_RATING,
+ RecordedPrograms.COLUMN_POSTER_ART_URI,
+ RecordedPrograms.COLUMN_THUMBNAIL_URI,
+ RecordedPrograms.COLUMN_SEARCHABLE,
+ RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
+ RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
+ RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
+ RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
+ RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
+ RecordedPrograms.COLUMN_VERSION_NUMBER,
+ RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
+ };
+
+ public static RecordedProgram fromCursor(Cursor cursor) {
+ int index = 0;
+ Builder builder = builder()
+ .setId(cursor.getLong(index++))
+ .setPackageName(cursor.getString(index++))
+ .setInputId(cursor.getString(index++))
+ .setChannelId(cursor.getLong(index++))
+ .setTitle(cursor.getString(index++))
+ .setSeasonNumber(cursor.getString(index++))
+ .setSeasonTitle(cursor.getString(index++))
+ .setEpisodeNumber(cursor.getString(index++))
+ .setEpisodeTitle(cursor.getString(index++))
+ .setStartTimeUtcMillis(cursor.getLong(index++))
+ .setEndTimeUtcMillis(cursor.getLong(index++))
+ .setBroadcastGenres(cursor.getString(index++))
+ .setCanonicalGenres(cursor.getString(index++))
+ .setShortDescription(cursor.getString(index++))
+ .setLongDescription(cursor.getString(index++))
+ .setVideoWidth(cursor.getInt(index++))
+ .setVideoHeight(cursor.getInt(index++))
+ .setAudioLanguage(cursor.getString(index++))
+ .setContentRating(cursor.getString(index++))
+ .setPosterArtUri(cursor.getString(index++))
+ .setThumbnailUri(cursor.getString(index++))
+ .setSearchable(cursor.getInt(index++) == 1)
+ .setDataUri(cursor.getString(index++))
+ .setDataBytes(cursor.getLong(index++))
+ .setDurationMillis(cursor.getLong(index++))
+ .setExpireTimeUtcMillis(cursor.getLong(index++))
+ .setInternalProviderFlag1(cursor.getInt(index++))
+ .setInternalProviderFlag2(cursor.getInt(index++))
+ .setInternalProviderFlag3(cursor.getInt(index++))
+ .setInternalProviderFlag4(cursor.getInt(index++))
+ .setVersionNumber(cursor.getInt(index++));
+ if (Utils.isInBundledPackageSet(builder.mPackageName)) {
+ InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
+ }
+ return builder.build();
+ }
+
+ public static ContentValues toValues(RecordedProgram recordedProgram) {
+ ContentValues values = new ContentValues();
+ if (recordedProgram.mId != ID_NOT_SET) {
+ values.put(RecordedPrograms._ID, recordedProgram.mId);
+ }
+ values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId);
+ values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId);
+ values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle);
+ values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber);
+ values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle);
+ values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber);
+ values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle);
+ values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
+ recordedProgram.mStartTimeUtcMillis);
+ values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis);
+ values.put(RecordedPrograms.COLUMN_BROADCAST_GENRE,
+ safeEncode(recordedProgram.mBroadcastGenres));
+ values.put(RecordedPrograms.COLUMN_CANONICAL_GENRE,
+ safeEncode(recordedProgram.mCanonicalGenres));
+ values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription);
+ values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription);
+ if (recordedProgram.mVideoWidth == 0) {
+ values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH);
+ } else {
+ values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth);
+ }
+ if (recordedProgram.mVideoHeight == 0) {
+ values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT);
+ } else {
+ values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight);
+ }
+ values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage);
+ values.put(RecordedPrograms.COLUMN_CONTENT_RATING, recordedProgram.mContentRating);
+ values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri);
+ values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri);
+ values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0);
+ values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI,
+ safeToString(recordedProgram.mDataUri));
+ values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes);
+ values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
+ recordedProgram.mDurationMillis);
+ values.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
+ recordedProgram.mExpireTimeUtcMillis);
+ values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
+ InternalDataUtils.serializeInternalProviderData(recordedProgram));
+ values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
+ recordedProgram.mInternalProviderFlag1);
+ values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ recordedProgram.mInternalProviderFlag2);
+ values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
+ recordedProgram.mInternalProviderFlag3);
+ values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
+ recordedProgram.mInternalProviderFlag4);
+ values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber);
+ return values;
+ }
+
+ public static class Builder{
+ private long mId = ID_NOT_SET;
+ private String mPackageName;
+ private String mInputId;
+ private long mChannelId;
+ private String mTitle;
+ private String mSeriesId;
+ private String mSeasonNumber;
+ private String mSeasonTitle;
+ private String mEpisodeNumber;
+ private String mEpisodeTitle;
+ private long mStartTimeUtcMillis;
+ private long mEndTimeUtcMillis;
+ private String[] mBroadcastGenres;
+ private String[] mCanonicalGenres;
+ private String mShortDescription;
+ private String mLongDescription;
+ private int mVideoWidth;
+ private int mVideoHeight;
+ private String mAudioLanguage;
+ private String mContentRating;
+ private String mPosterArtUri;
+ private String mThumbnailUri;
+ private boolean mSearchable = true;
+ private Uri mDataUri;
+ private long mDataBytes;
+ private long mDurationMillis;
+ private long mExpireTimeUtcMillis;
+ private int mInternalProviderFlag1;
+ private int mInternalProviderFlag2;
+ private int mInternalProviderFlag3;
+ private int mInternalProviderFlag4;
+ private int mVersionNumber;
+
+ public Builder setId(long id) {
+ mId = id;
+ return this;
+ }
+
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ public Builder setInputId(String inputId) {
+ mInputId = inputId;
+ return this;
+ }
+
+ public Builder setChannelId(long channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setSeriesId(String seriesId) {
+ mSeriesId = seriesId;
+ return this;
+ }
+
+ public Builder setSeasonNumber(String seasonNumber) {
+ mSeasonNumber = seasonNumber;
+ return this;
+ }
+
+ public Builder setSeasonTitle(String seasonTitle) {
+ mSeasonTitle = seasonTitle;
+ return this;
+ }
+
+ public Builder setEpisodeNumber(String episodeNumber) {
+ mEpisodeNumber = episodeNumber;
+ return this;
+ }
+
+ public Builder setEpisodeTitle(String episodeTitle) {
+ mEpisodeTitle = episodeTitle;
+ return this;
+ }
+
+ public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
+ mStartTimeUtcMillis = startTimeUtcMillis;
+ return this;
+ }
+
+ public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
+ mEndTimeUtcMillis = endTimeUtcMillis;
+ return this;
+ }
+
+ public Builder setBroadcastGenres(String broadcastGenres) {
+ if (TextUtils.isEmpty(broadcastGenres)) {
+ mBroadcastGenres = null;
+ return this;
+ }
+ return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres));
+ }
+
+ private Builder setBroadcastGenres(String[] broadcastGenres) {
+ mBroadcastGenres = broadcastGenres;
+ return this;
+ }
+
+ public Builder setCanonicalGenres(String canonicalGenres) {
+ if (TextUtils.isEmpty(canonicalGenres)) {
+ mCanonicalGenres = null;
+ return this;
+ }
+ return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres));
+ }
+
+ private Builder setCanonicalGenres(String[] canonicalGenres) {
+ mCanonicalGenres = canonicalGenres;
+ return this;
+ }
+
+ public Builder setShortDescription(String shortDescription) {
+ mShortDescription = shortDescription;
+ return this;
+ }
+
+ public Builder setLongDescription(String longDescription) {
+ mLongDescription = longDescription;
+ return this;
+ }
+
+ public Builder setVideoWidth(int videoWidth) {
+ mVideoWidth = videoWidth;
+ return this;
+ }
+
+ public Builder setVideoHeight(int videoHeight) {
+ mVideoHeight = videoHeight;
+ return this;
+ }
+
+ public Builder setAudioLanguage(String audioLanguage) {
+ mAudioLanguage = audioLanguage;
+ return this;
+ }
+
+ public Builder setContentRating(String contentRating) {
+ mContentRating = contentRating;
+ return this;
+ }
+
+ private Uri toUri(String uriString) {
+ try {
+ return uriString == null ? null : Uri.parse(uriString);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public Builder setPosterArtUri(String posterArtUri) {
+ mPosterArtUri = posterArtUri;
+ return this;
+ }
+
+ public Builder setThumbnailUri(String thumbnailUri) {
+ mThumbnailUri = thumbnailUri;
+ return this;
+ }
+
+ public Builder setSearchable(boolean searchable) {
+ mSearchable = searchable;
+ return this;
+ }
+
+ public Builder setDataUri(String dataUri) {
+ return setDataUri(toUri(dataUri));
+ }
+
+ public Builder setDataUri(Uri dataUri) {
+ mDataUri = dataUri;
+ return this;
+ }
+
+ public Builder setDataBytes(long dataBytes) {
+ mDataBytes = dataBytes;
+ return this;
+ }
+
+ public Builder setDurationMillis(long durationMillis) {
+ mDurationMillis = durationMillis;
+ return this;
+ }
+
+ public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) {
+ mExpireTimeUtcMillis = expireTimeUtcMillis;
+ return this;
+ }
+
+ public Builder setInternalProviderFlag1(int internalProviderFlag1) {
+ mInternalProviderFlag1 = internalProviderFlag1;
+ return this;
+ }
+
+ public Builder setInternalProviderFlag2(int internalProviderFlag2) {
+ mInternalProviderFlag2 = internalProviderFlag2;
+ return this;
+ }
+
+ public Builder setInternalProviderFlag3(int internalProviderFlag3) {
+ mInternalProviderFlag3 = internalProviderFlag3;
+ return this;
+ }
+
+ public Builder setInternalProviderFlag4(int internalProviderFlag4) {
+ mInternalProviderFlag4 = internalProviderFlag4;
+ return this;
+ }
+
+ public Builder setVersionNumber(int versionNumber) {
+ mVersionNumber = versionNumber;
+ return this;
+ }
+
+ public RecordedProgram build() {
+ // Generate the series ID for the episodic program of other TV input.
+ if (TextUtils.isEmpty(mSeriesId)
+ && !TextUtils.isEmpty(mEpisodeNumber)) {
+ setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle));
+ }
+ return new RecordedProgram(mId, mPackageName, mInputId, mChannelId, mTitle, mSeriesId,
+ mSeasonNumber, mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis,
+ mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription,
+ mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRating,
+ mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes,
+ mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1,
+ mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4,
+ mVersionNumber);
+ }
+ }
+
+ public static Builder builder() { return new Builder(); }
+
+ public static Builder buildFrom(RecordedProgram orig) {
+ return builder()
+ .setId(orig.getId())
+ .setPackageName(orig.getPackageName())
+ .setInputId(orig.getInputId())
+ .setChannelId(orig.getChannelId())
+ .setTitle(orig.getTitle())
+ .setSeriesId(orig.getSeriesId())
+ .setSeasonNumber(orig.getSeasonNumber())
+ .setSeasonTitle(orig.getSeasonTitle())
+ .setEpisodeNumber(orig.getEpisodeNumber())
+ .setEpisodeTitle(orig.getEpisodeTitle())
+ .setStartTimeUtcMillis(orig.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(orig.getEndTimeUtcMillis())
+ .setBroadcastGenres(orig.getBroadcastGenres())
+ .setCanonicalGenres(orig.getCanonicalGenres())
+ .setShortDescription(orig.getDescription())
+ .setLongDescription(orig.getLongDescription())
+ .setVideoWidth(orig.getVideoWidth())
+ .setVideoHeight(orig.getVideoHeight())
+ .setAudioLanguage(orig.getAudioLanguage())
+ .setContentRating(orig.getContentRating())
+ .setPosterArtUri(orig.getPosterArtUri())
+ .setThumbnailUri(orig.getThumbnailUri())
+ .setSearchable(orig.isSearchable())
+ .setInternalProviderFlag1(orig.getInternalProviderFlag1())
+ .setInternalProviderFlag2(orig.getInternalProviderFlag2())
+ .setInternalProviderFlag3(orig.getInternalProviderFlag3())
+ .setInternalProviderFlag4(orig.getInternalProviderFlag4())
+ .setVersionNumber(orig.getVersionNumber());
+ }
+
+ public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR =
+ new Comparator<RecordedProgram>() {
+ @Override
+ public int compare(RecordedProgram lhs, RecordedProgram rhs) {
+ int res =
+ Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
+ if (res != 0) {
+ return res;
+ }
+ return Long.compare(lhs.mId, rhs.mId);
+ }
+ };
+
+ private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
+
+ private final long mId;
+ private final String mPackageName;
+ private final String mInputId;
+ private final long mChannelId;
+ private final String mTitle;
+ private final String mSeriesId;
+ private final String mSeasonNumber;
+ private final String mSeasonTitle;
+ private final String mEpisodeNumber;
+ private final String mEpisodeTitle;
+ private final long mStartTimeUtcMillis;
+ private final long mEndTimeUtcMillis;
+ private final String[] mBroadcastGenres;
+ private final String[] mCanonicalGenres;
+ private final String mShortDescription;
+ private final String mLongDescription;
+ private final int mVideoWidth;
+ private final int mVideoHeight;
+ private final String mAudioLanguage;
+ private final String mContentRating;
+ private final String mPosterArtUri;
+ private final String mThumbnailUri;
+ private final boolean mSearchable;
+ private final Uri mDataUri;
+ private final long mDataBytes;
+ private final long mDurationMillis;
+ private final long mExpireTimeUtcMillis;
+ private final int mInternalProviderFlag1;
+ private final int mInternalProviderFlag2;
+ private final int mInternalProviderFlag3;
+ private final int mInternalProviderFlag4;
+ private final int mVersionNumber;
+
+ private RecordedProgram(long id, String packageName, String inputId, long channelId,
+ String title, String seriesId, String seasonNumber, String seasonTitle,
+ String episodeNumber, String episodeTitle, long startTimeUtcMillis,
+ long endTimeUtcMillis, String[] broadcastGenres, String[] canonicalGenres,
+ String shortDescription, String longDescription, int videoWidth, int videoHeight,
+ String audioLanguage, String contentRating, String posterArtUri, String thumbnailUri,
+ boolean searchable, Uri dataUri, long dataBytes, long durationMillis,
+ long expireTimeUtcMillis, int internalProviderFlag1, int internalProviderFlag2,
+ int internalProviderFlag3, int internalProviderFlag4, int versionNumber) {
+ mId = id;
+ mPackageName = packageName;
+ mInputId = inputId;
+ mChannelId = channelId;
+ mTitle = title;
+ mSeriesId = seriesId;
+ mSeasonNumber = seasonNumber;
+ mSeasonTitle = seasonTitle;
+ mEpisodeNumber = episodeNumber;
+ mEpisodeTitle = episodeTitle;
+ mStartTimeUtcMillis = startTimeUtcMillis;
+ mEndTimeUtcMillis = endTimeUtcMillis;
+ mBroadcastGenres = broadcastGenres;
+ mCanonicalGenres = canonicalGenres;
+ mShortDescription = shortDescription;
+ mLongDescription = longDescription;
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+
+ mAudioLanguage = audioLanguage;
+ mContentRating = contentRating;
+ mPosterArtUri = posterArtUri;
+ mThumbnailUri = thumbnailUri;
+ mSearchable = searchable;
+ mDataUri = dataUri;
+ mDataBytes = dataBytes;
+ mDurationMillis = durationMillis;
+ mExpireTimeUtcMillis = expireTimeUtcMillis;
+ mInternalProviderFlag1 = internalProviderFlag1;
+ mInternalProviderFlag2 = internalProviderFlag2;
+ mInternalProviderFlag3 = internalProviderFlag3;
+ mInternalProviderFlag4 = internalProviderFlag4;
+ mVersionNumber = versionNumber;
+ }
+
+ public String getAudioLanguage() {
+ return mAudioLanguage;
+ }
+
+ public String[] getBroadcastGenres() {
+ return mBroadcastGenres;
+ }
+
+ public String[] getCanonicalGenres() {
+ return mCanonicalGenres;
+ }
+
+ @Override
+ public long getChannelId() {
+ return mChannelId;
+ }
+
+ public String getContentRating() {
+ return mContentRating;
+ }
+
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ public long getDataBytes() {
+ return mDataBytes;
+ }
+
+ @Override
+ public long getDurationMillis() {
+ return mDurationMillis;
+ }
+
+ @Override
+ public long getEndTimeUtcMillis() {
+ return mEndTimeUtcMillis;
+ }
+
+ @Override
+ public String getEpisodeNumber() {
+ return mEpisodeNumber;
+ }
+
+ public String getEpisodeTitle() {
+ return mEpisodeTitle;
+ }
+
+ @Override
+ public String getEpisodeDisplayTitle(Context context) {
+ if (!TextUtils.isEmpty(mEpisodeNumber)) {
+ String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle;
+ if (TextUtils.equals(mSeasonNumber, "0")) {
+ // Do not show "S0: ".
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format_no_season_number),
+ mEpisodeNumber, episodeTitle);
+ } else {
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format),
+ mSeasonNumber, mEpisodeNumber, episodeTitle);
+ }
+ }
+ return mEpisodeTitle;
+ }
+
+ @Nullable
+ @Override
+ public String getTitleWithEpisodeNumber(Context context) {
+ if (TextUtils.isEmpty(mTitle)) {
+ return mTitle;
+ }
+ if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) {
+ return TextUtils.isEmpty(mEpisodeNumber) ? mTitle : context.getString(
+ R.string.program_title_with_episode_number_no_season, mTitle, mEpisodeNumber);
+ } else {
+ return context.getString(R.string.program_title_with_episode_number, mTitle,
+ mSeasonNumber, mEpisodeNumber);
+ }
+ }
+
+ public long getExpireTimeUtcMillis() {
+ return mExpireTimeUtcMillis;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public String getInputId() {
+ return mInputId;
+ }
+
+ public int getInternalProviderFlag1() {
+ return mInternalProviderFlag1;
+ }
+
+ public int getInternalProviderFlag2() {
+ return mInternalProviderFlag2;
+ }
+
+ public int getInternalProviderFlag3() {
+ return mInternalProviderFlag3;
+ }
+
+ public int getInternalProviderFlag4() {
+ return mInternalProviderFlag4;
+ }
+
+ @Override
+ public String getDescription() {
+ return mShortDescription;
+ }
+
+ @Override
+ public String getLongDescription() {
+ return mLongDescription;
+ }
+
+ @Override
+ public String getPosterArtUri() {
+ return mPosterArtUri;
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
+ }
+
+ public boolean isSearchable() {
+ return mSearchable;
+ }
+
+ public String getSeriesId() {
+ return mSeriesId;
+ }
+
+ @Override
+ public String getSeasonNumber() {
+ return mSeasonNumber;
+ }
+
+ public String getSeasonTitle() {
+ return mSeasonTitle;
+ }
+
+ @Override
+ public long getStartTimeUtcMillis() {
+ return mStartTimeUtcMillis;
+ }
+
+ @Override
+ public String getThumbnailUri() {
+ return mThumbnailUri;
+ }
+
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public Uri getUri() {
+ return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId);
+ }
+
+ public int getVersionNumber() {
+ return mVersionNumber;
+ }
+
+ public int getVideoHeight() {
+ return mVideoHeight;
+ }
+
+ public int getVideoWidth() {
+ return mVideoWidth;
+ }
+
+ /**
+ * Checks whether the recording has been clipped or not.
+ */
+ public boolean isClipped() {
+ return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RecordedProgram that = (RecordedProgram) o;
+ return Objects.equals(mId, that.mId) &&
+ Objects.equals(mChannelId, that.mChannelId) &&
+ Objects.equals(mSeriesId, that.mSeriesId) &&
+ Objects.equals(mSeasonNumber, that.mSeasonNumber) &&
+ Objects.equals(mSeasonTitle, that.mSeasonTitle) &&
+ Objects.equals(mEpisodeNumber, that.mEpisodeNumber) &&
+ Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) &&
+ Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) &&
+ Objects.equals(mVideoWidth, that.mVideoWidth) &&
+ Objects.equals(mVideoHeight, that.mVideoHeight) &&
+ Objects.equals(mSearchable, that.mSearchable) &&
+ Objects.equals(mDataBytes, that.mDataBytes) &&
+ Objects.equals(mDurationMillis, that.mDurationMillis) &&
+ Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) &&
+ Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) &&
+ Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) &&
+ Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) &&
+ Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) &&
+ Objects.equals(mVersionNumber, that.mVersionNumber) &&
+ Objects.equals(mTitle, that.mTitle) &&
+ Objects.equals(mEpisodeTitle, that.mEpisodeTitle) &&
+ Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) &&
+ Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) &&
+ Objects.equals(mShortDescription, that.mShortDescription) &&
+ Objects.equals(mLongDescription, that.mLongDescription) &&
+ Objects.equals(mAudioLanguage, that.mAudioLanguage) &&
+ Objects.equals(mContentRating, that.mContentRating) &&
+ Objects.equals(mPosterArtUri, that.mPosterArtUri) &&
+ Objects.equals(mThumbnailUri, that.mThumbnailUri);
+ }
+
+ /**
+ * Hashes based on the ID.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId);
+ }
+
+ @Override
+ public String toString() {
+ return "RecordedProgram"
+ + "[" + mId +
+ "]{ mPackageName=" + mPackageName +
+ ", mInputId='" + mInputId + '\'' +
+ ", mChannelId='" + mChannelId + '\'' +
+ ", mTitle='" + mTitle + '\'' +
+ ", mSeriesId='" + mSeriesId + '\'' +
+ ", mEpisodeNumber=" + mEpisodeNumber +
+ ", mEpisodeTitle='" + mEpisodeTitle + '\'' +
+ ", mStartTimeUtcMillis=" + mStartTimeUtcMillis +
+ ", mEndTimeUtcMillis=" + mEndTimeUtcMillis +
+ ", mBroadcastGenres=" +
+ (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") +
+ ", mCanonicalGenres=" +
+ (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") +
+ ", mShortDescription='" + mShortDescription + '\'' +
+ ", mLongDescription='" + mLongDescription + '\'' +
+ ", mVideoHeight=" + mVideoHeight +
+ ", mVideoWidth=" + mVideoWidth +
+ ", mAudioLanguage='" + mAudioLanguage + '\'' +
+ ", mContentRating='" + mContentRating + '\'' +
+ ", mPosterArtUri=" + mPosterArtUri +
+ ", mThumbnailUri=" + mThumbnailUri +
+ ", mSearchable=" + mSearchable +
+ ", mDataUri=" + mDataUri +
+ ", mDataBytes=" + mDataBytes +
+ ", mDurationMillis=" + mDurationMillis +
+ ", mExpireTimeUtcMillis=" + mExpireTimeUtcMillis +
+ ", mInternalProviderFlag1=" + mInternalProviderFlag1 +
+ ", mInternalProviderFlag2=" + mInternalProviderFlag2 +
+ ", mInternalProviderFlag3=" + mInternalProviderFlag3 +
+ ", mInternalProviderFlag4=" + mInternalProviderFlag4 +
+ ", mSeasonNumber=" + mSeasonNumber +
+ ", mSeasonTitle=" + mSeasonTitle +
+ ", mVersionNumber=" + mVersionNumber +
+ '}';
+ }
+
+ @Nullable
+ private static String safeToString(@Nullable Object o) {
+ return o == null ? null : o.toString();
+ }
+
+ @Nullable
+ private static String safeEncode(@Nullable String[] genres) {
+ return genres == null ? null : TvContract.Programs.Genres.encode(genres);
+ }
+}
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java
index 804485b3..2373f15c 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/RecordingTask.java
@@ -16,18 +16,28 @@
package com.android.tv.dvr;
+import android.annotation.TargetApi;
+import android.content.Context;
import android.media.tv.TvContract;
-import android.media.tv.TvRecordingClient;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvRecordingClient.RecordingCallback;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import android.widget.Toast;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.RecordingSession;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
+import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper;
import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
@@ -40,22 +50,33 @@ import java.util.concurrent.TimeUnit;
* There is only one looper so messages must be handled quickly or start a separate thread.
*/
@WorkerThread
-class RecordingTask extends TvRecordingClient.RecordingCallback
- implements Handler.Callback, DvrManager.Listener {
+@VisibleForTesting
+@TargetApi(Build.VERSION_CODES.N)
+public class RecordingTask extends RecordingCallback implements Handler.Callback,
+ DvrManager.Listener {
private static final String TAG = "RecordingTask";
private static final boolean DEBUG = false;
@VisibleForTesting
- static final int MESSAGE_INIT = 1;
+ static final int MSG_INITIALIZE = 1;
@VisibleForTesting
- static final int MESSAGE_START_RECORDING = 2;
+ static final int MSG_START_RECORDING = 2;
@VisibleForTesting
- static final int MESSAGE_STOP_RECORDING = 3;
-
- @VisibleForTesting
- static final long MS_BEFORE_START = TimeUnit.SECONDS.toMillis(5);
- @VisibleForTesting
- static final long MS_AFTER_END = TimeUnit.SECONDS.toMillis(5);
+ static final int MSG_STOP_RECORDING = 3;
+ /**
+ * Message to update schedule.
+ */
+ public static final int MSG_UDPATE_SCHEDULE = 4;
+
+ /**
+ * The time when the start command will be sent before the recording starts.
+ */
+ public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3);
+ /**
+ * If the recording starts later than the scheduled start time or ends before the scheduled end
+ * time, it's considered as clipped.
+ */
+ private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
@VisibleForTesting
enum State {
@@ -63,27 +84,32 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
SESSION_ACQUIRED,
CONNECTION_PENDING,
CONNECTED,
- RECORDING_START_REQUESTED,
RECORDING_STARTED,
RECORDING_STOP_REQUESTED,
+ FINISHED,
ERROR,
RELEASED,
}
- private final DvrSessionManager mSessionManager;
+ private final InputSessionManager mSessionManager;
private final DvrManager mDvrManager;
+ private final Context mContext;
private final WritableDvrDataManager mDataManager;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private TvRecordingClient mTvRecordingClient;
+ private RecordingSession mRecordingSession;
private Handler mHandler;
private ScheduledRecording mScheduledRecording;
private final Channel mChannel;
private State mState = State.NOT_STARTED;
private final Clock mClock;
+ private boolean mStartedWithClipping;
+ private Uri mRecordedProgramUri;
+ private boolean mCanceled;
- RecordingTask(ScheduledRecording scheduledRecording, Channel channel,
- DvrManager dvrManager, DvrSessionManager sessionManager,
+ RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel,
+ DvrManager dvrManager, InputSessionManager sessionManager,
WritableDvrDataManager dataManager, Clock clock) {
+ mContext = context;
mScheduledRecording = scheduledRecording;
mChannel = channel;
mSessionManager = sessionManager;
@@ -101,27 +127,30 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
@Override
public boolean handleMessage(Message msg) {
if (DEBUG) Log.d(TAG, "handleMessage " + msg);
- SoftPreconditions
- .checkState(msg.what == Scheduler.HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
- TAG, "Null handler trying to handle " + msg);
+ SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null,
+ TAG, "Null handler trying to handle " + msg);
try {
switch (msg.what) {
- case MESSAGE_INIT:
+ case MSG_INITIALIZE:
handleInit();
break;
- case MESSAGE_START_RECORDING:
+ case MSG_START_RECORDING:
handleStartRecording();
break;
- case MESSAGE_STOP_RECORDING:
+ case MSG_STOP_RECORDING:
handleStopRecording();
break;
- case Scheduler.HandlerWrapper.MESSAGE_REMOVE:
- // Clear the handler
+ case MSG_UDPATE_SCHEDULE:
+ handleUpdateSchedule((ScheduledRecording) msg.obj);
+ break;
+ case HandlerWrapper.MESSAGE_REMOVE:
+ mHandler.removeCallbacksAndMessages(null);
mHandler = null;
release();
return false;
default:
SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg);
+ break;
}
return true;
} catch (Exception e) {
@@ -132,54 +161,83 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
}
@Override
+ public void onDisconnected(String inputId) {
+ if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")");
+ if (mRecordingSession != null && mState != State.FINISHED) {
+ failAndQuit();
+ }
+ }
+
+ @Override
public void onTuned(Uri channelUri) {
- if (DEBUG) {
- Log.d(TAG, "onTuned");
+ if (DEBUG) Log.d(TAG, "onTuned");
+ if (mRecordingSession == null) {
+ return;
}
- super.onTuned(channelUri);
mState = State.CONNECTED;
- if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_START_RECORDING,
- mScheduledRecording.getStartTimeMs() - MS_BEFORE_START)) {
- mState = State.ERROR;
- return;
+ if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING,
+ mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) {
+ failAndQuit();
}
}
-
@Override
public void onRecordingStopped(Uri recordedProgramUri) {
- super.onRecordingStopped(recordedProgramUri);
- mState = State.CONNECTED;
- updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
- .setState(ScheduledRecording.STATE_RECORDING_FINISHED).build());
+ if (DEBUG) Log.d(TAG, "onRecordingStopped");
+ if (mRecordingSession == null) {
+ return;
+ }
+ mRecordedProgramUri = recordedProgramUri;
+ mState = State.FINISHED;
+ int state = ScheduledRecording.STATE_RECORDING_FINISHED;
+ if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS
+ > mClock.currentTimeMillis()) {
+ state = ScheduledRecording.STATE_RECORDING_CLIPPED;
+ }
+ updateRecordingState(state);
sendRemove();
+ if (mCanceled) {
+ removeRecordedProgram();
+ }
}
@Override
public void onError(int reason) {
if (DEBUG) Log.d(TAG, "onError reason " + reason);
- super.onError(reason);
- // TODO(dvr) handle success
+ if (mRecordingSession == null) {
+ return;
+ }
switch (reason) {
+ case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE:
+ mMainThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (TvApplication.getSingletons(mContext).getMainActivityWrapper()
+ .isResumed()) {
+ Toast.makeText(mContext.getApplicationContext(),
+ R.string.dvr_error_insufficient_space_description,
+ Toast.LENGTH_LONG)
+ .show();
+ } else {
+ Utils.setRecordingFailedReason(mContext.getApplicationContext(),
+ TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ }
+ }
+ });
+ // Pass through
default:
- updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
- .setState(ScheduledRecording.STATE_RECORDING_FAILED)
- .build());
+ failAndQuit();
+ break;
}
- release();
- sendRemove();
}
private void handleInit() {
if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording);
- //TODO check recording preconditions
-
if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) {
Log.w(TAG, "End time already past, not recording " + mScheduledRecording);
failAndQuit();
return;
}
-
if (mChannel == null) {
Log.w(TAG, "Null channel for " + mScheduledRecording);
failAndQuit();
@@ -193,18 +251,11 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
}
String inputId = mChannel.getInputId();
- if (mSessionManager.canAcquireDvrSession(inputId, mChannel)) {
- mTvRecordingClient = mSessionManager
- .createTvRecordingClient("recordingTask-" + mScheduledRecording.getId(), this,
- mHandler);
- mState = State.SESSION_ACQUIRED;
- } else {
- Log.w(TAG, "Unable to acquire a session for " + mScheduledRecording);
- failAndQuit();
- return;
- }
+ mRecordingSession = mSessionManager.createRecordingSession(inputId,
+ "recordingTask-" + mScheduledRecording.getId(), this, mHandler);
+ mState = State.SESSION_ACQUIRED;
mDvrManager.addListener(this, mHandler);
- mTvRecordingClient.tune(inputId, mChannel.getUri());
+ mRecordingSession.tune(inputId, mChannel.getUri());
mState = State.CONNECTION_PENDING;
}
@@ -218,41 +269,78 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
private void sendRemove() {
if (DEBUG) Log.d(TAG, "sendRemove");
if (mHandler != null) {
- mHandler.sendEmptyMessage(Scheduler.HandlerWrapper.MESSAGE_REMOVE);
+ mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(
+ HandlerWrapper.MESSAGE_REMOVE));
}
}
private void handleStartRecording() {
if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording);
- // TODO(DVR) handle errors
long programId = mScheduledRecording.getProgramId();
- mTvRecordingClient.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null
+ mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null
: TvContract.buildProgramUri(programId));
- updateRecording(ScheduledRecording.buildFrom(mScheduledRecording)
- .setState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS).build());
+ updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS);
+ // If it starts late, it's clipped.
+ if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS
+ < mClock.currentTimeMillis()) {
+ mStartedWithClipping = true;
+ }
mState = State.RECORDING_STARTED;
- if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MESSAGE_STOP_RECORDING,
- mScheduledRecording.getEndTimeMs() + MS_AFTER_END)) {
- mState = State.ERROR;
- return;
+ if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING,
+ mScheduledRecording.getEndTimeMs())) {
+ failAndQuit();
}
}
private void handleStopRecording() {
if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording);
- mTvRecordingClient.stopRecording();
+ mRecordingSession.stopRecording();
mState = State.RECORDING_STOP_REQUESTED;
}
+ 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();
+ }
+ }
+ }
+
@VisibleForTesting
State getState() {
return mState;
}
+ /**
+ * Returns the priority.
+ */
+ public long getPriority() {
+ return mScheduledRecording.getPriority();
+ }
+
+ /**
+ * Returns the start time of the recording.
+ */
+ public long getStartTimeMs() {
+ return mScheduledRecording.getStartTimeMs();
+ }
+
+ /**
+ * Returns the end time of the recording.
+ */
+ public long getEndTimeMs() {
+ return mScheduledRecording.getEndTimeMs();
+ }
+
private void release() {
- if (mTvRecordingClient != null) {
- mSessionManager.releaseTvRecordingClient(mTvRecordingClient);
+ if (mRecordingSession != null) {
+ mSessionManager.releaseRecordingSession(mRecordingSession);
+ mRecordingSession = null;
}
mDvrManager.removeListener(this);
}
@@ -268,22 +356,24 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
}
private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
- updateRecording(ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build());
- }
-
- @VisibleForTesting
- static Uri getIdAsMediaUri(ScheduledRecording scheduledRecording) {
- // TODO define the URI format
- return new Uri.Builder().appendPath(String.valueOf(scheduledRecording.getId())).build();
- }
-
- private void updateRecording(ScheduledRecording updatedScheduledRecording) {
- if (DEBUG) Log.d(TAG, "updateScheduledRecording " + updatedScheduledRecording);
- mScheduledRecording = updatedScheduledRecording;
+ if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state);
+ mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state)
+ .build();
mMainThreadHandler.post(new Runnable() {
@Override
public void run() {
- mDataManager.updateScheduledRecording(mScheduledRecording);
+ ScheduledRecording schedule = mDataManager.getScheduledRecording(
+ mScheduledRecording.getId());
+ if (schedule == null) {
+ // Schedule has been deleted. Delete the recorded program.
+ removeRecordedProgram();
+ } else {
+ // Update the state based on the object in DataManager in case when it has been
+ // updated. mScheduledRecording will be updated from
+ // onScheduledRecordingStateChanged.
+ mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule)
+ .setState(state).build());
+ }
}
});
}
@@ -293,9 +383,24 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
if (recording.getId() != mScheduledRecording.getId()) {
return;
}
+ stop();
+ }
+
+ /**
+ * Starts the task.
+ */
+ public void start() {
+ mHandler.sendEmptyMessage(MSG_INITIALIZE);
+ }
+
+ /**
+ * Stops the task.
+ */
+ public void stop() {
+ if (DEBUG) Log.d(TAG, "stop");
switch (mState) {
case RECORDING_STARTED:
- mHandler.removeMessages(MESSAGE_STOP_RECORDING);
+ mHandler.removeMessages(MSG_STOP_RECORDING);
handleStopRecording();
break;
case RECORDING_STOP_REQUESTED:
@@ -305,7 +410,7 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
case SESSION_ACQUIRED:
case CONNECTION_PENDING:
case CONNECTED:
- case RECORDING_START_REQUESTED:
+ case FINISHED:
case ERROR:
case RELEASED:
default:
@@ -314,8 +419,37 @@ class RecordingTask extends TvRecordingClient.RecordingCallback
}
}
+ /**
+ * Cancels the task
+ */
+ public void cancel() {
+ if (DEBUG) Log.d(TAG, "cancel");
+ mCanceled = true;
+ stop();
+ removeRecordedProgram();
+ }
+
@Override
public String toString() {
return getClass().getName() + "(" + mScheduledRecording + ")";
}
+
+ private void removeRecordedProgram() {
+ runOnMainThread(new Runnable() {
+ @Override
+ public void run() {
+ if (mRecordedProgramUri != null) {
+ mDvrManager.removeRecordedProgram(mRecordedProgramUri);
+ }
+ }
+ });
+ }
+
+ private void runOnMainThread(Runnable runnable) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ mMainThreadHandler.post(runnable);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/ScheduledProgramReaper.java
index 9053eaec..cd79a631 100644
--- a/src/com/android/tv/dvr/ScheduledProgramReaper.java
+++ b/src/com/android/tv/dvr/ScheduledProgramReaper.java
@@ -21,6 +21,7 @@ import android.support.annotation.VisibleForTesting;
import com.android.tv.util.Clock;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -42,12 +43,25 @@ class ScheduledProgramReaper implements Runnable {
@Override
@MainThread
public void run() {
- List<ScheduledRecording> recordings = mDvrDataManager.getAllScheduledRecordings();
long cutoff = mClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(DAYS);
- for (ScheduledRecording r : recordings) {
+ List<ScheduledRecording> toRemove = new ArrayList<>();
+ for (ScheduledRecording r : mDvrDataManager.getAllScheduledRecordings()) {
+ // Do not remove the schedules if it belongs to the series recording and was finished
+ // successfully. The schedule is necessary for checking the scheduled episode of the
+ // series recording.
+ if (r.getEndTimeMs() < cutoff
+ && (r.getSeriesRecordingId() == SeriesRecording.ID_NOT_SET
+ || r.getState() != ScheduledRecording.STATE_RECORDING_FINISHED)) {
+ toRemove.add(r);
+ }
+ }
+ for (ScheduledRecording r : mDvrDataManager.getDeletedSchedules()) {
if (r.getEndTimeMs() < cutoff) {
- mDvrDataManager.removeScheduledRecording(r);
+ toRemove.add(r);
}
}
+ if (!toRemove.isEmpty()) {
+ mDvrDataManager.removeScheduledRecording(ScheduledRecording.toArray(toRemove));
+ }
}
}
diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java
index 01b00459..a9673b40 100644
--- a/src/com/android/tv/dvr/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/ScheduledRecording.java
@@ -17,46 +17,75 @@
package com.android.tv.dvr;
import android.content.ContentValues;
+import android.content.Context;
import android.database.Cursor;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
import android.util.Range;
+import com.android.tv.R;
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;
+import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
import java.util.Comparator;
+import java.util.Objects;
/**
* A data class for one recording contents.
*/
@VisibleForTesting
-public final class ScheduledRecording {
- private static final String TAG = "Recording";
+public final class ScheduledRecording implements Parcelable {
+ private static final String TAG = "ScheduledRecording";
- public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id"; //TODO(DVR) move
- public static final String PARAM_INPUT_ID = "input_id";
+ /**
+ * Indicates that the ID is not assigned yet.
+ */
+ public static final long ID_NOT_SET = 0;
- public static final long ID_NOT_SET = -1;
+ /**
+ * The default priority of the recording.
+ */
+ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
- public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() {
+ public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR
+ = new Comparator<ScheduledRecording>() {
@Override
public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
}
};
- public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() {
+ /**
+ * Compare the end time in ascending order.
+ */
+ public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR
+ = new Comparator<ScheduledRecording>() {
+ @Override
+ public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
+ return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
+ }
+ };
+
+ /**
+ * Compare priority in descending order.
+ */
+ public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR
+ = new Comparator<ScheduledRecording>() {
@Override
public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- int value = Long.compare(lhs.mPriority, rhs.mPriority);
+ int value = Long.compare(rhs.mPriority, lhs.mPriority);
if (value == 0) {
- value = Long.compare(lhs.mId, rhs.mId);
+ // New recording has the higher priority.
+ value = Long.compare(rhs.mId, lhs.mId);
}
return value;
}
@@ -74,30 +103,77 @@ public final class ScheduledRecording {
}
};
- public static Builder builder(Program p) {
+ /**
+ * Builds scheduled recordings from programs.
+ */
+ public static Builder builder(String inputId, Program p) {
return new Builder()
- .setStartTime(p.getStartTimeUtcMillis()).setEndTime(p.getEndTimeUtcMillis())
+ .setInputId(inputId)
+ .setChannelId(p.getChannelId())
+ .setStartTimeMs(p.getStartTimeUtcMillis()).setEndTimeMs(p.getEndTimeUtcMillis())
.setProgramId(p.getId())
+ .setProgramTitle(p.getTitle())
+ .setSeasonNumber(p.getSeasonNumber())
+ .setEpisodeNumber(p.getEpisodeNumber())
+ .setEpisodeTitle(p.getEpisodeTitle())
+ .setProgramDescription(p.getDescription())
+ .setProgramLongDescription(p.getLongDescription())
+ .setProgramPosterArtUri(p.getPosterArtUri())
+ .setProgramThumbnailUri(p.getThumbnailUri())
.setType(TYPE_PROGRAM);
}
- public static Builder builder(long startTime, long endTime) {
+ public static Builder builder(String inputId, long channelId, long startTime, long endTime) {
return new Builder()
- .setStartTime(startTime)
- .setEndTime(endTime)
+ .setInputId(inputId)
+ .setChannelId(channelId)
+ .setStartTimeMs(startTime)
+ .setEndTimeMs(endTime)
.setType(TYPE_TIMED);
}
+ /**
+ * Creates a new Builder with the values set from the {@link RecordedProgram}.
+ */
+ @VisibleForTesting
+ public static Builder builder(RecordedProgram p) {
+ boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle());
+ return new Builder()
+ .setInputId(p.getInputId())
+ .setChannelId(p.getChannelId())
+ .setType(isProgramRecording ? TYPE_PROGRAM : TYPE_TIMED)
+ .setStartTimeMs(p.getStartTimeUtcMillis())
+ .setEndTimeMs(p.getEndTimeUtcMillis())
+ .setProgramTitle(p.getTitle())
+ .setSeasonNumber(p.getSeasonNumber())
+ .setEpisodeNumber(p.getEpisodeNumber())
+ .setEpisodeTitle(p.getEpisodeTitle())
+ .setProgramDescription(p.getDescription())
+ .setProgramLongDescription(p.getLongDescription())
+ .setProgramPosterArtUri(p.getPosterArtUri())
+ .setProgramThumbnailUri(p.getThumbnailUri())
+ .setState(STATE_RECORDING_FINISHED);
+ }
+
public static final class Builder {
private long mId = ID_NOT_SET;
- private long mPriority = Long.MAX_VALUE;
+ private long mPriority = DvrScheduleManager.DEFAULT_PRIORITY;
+ private String mInputId;
private long mChannelId;
private long mProgramId = ID_NOT_SET;
+ private String mProgramTitle;
private @RecordingType int mType;
- private long mStartTime;
- private long mEndTime;
+ private long mStartTimeMs;
+ private long mEndTimeMs;
+ private String mSeasonNumber;
+ private String mEpisodeNumber;
+ private String mEpisodeTitle;
+ private String mProgramDescription;
+ private String mProgramLongDescription;
+ private String mProgramPosterArtUri;
+ private String mProgramThumbnailUri;
private @RecordingState int mState;
- private SeasonRecording mParentSeasonRecording;
+ private long mSeriesRecordingId = ID_NOT_SET;
private Builder() { }
@@ -111,6 +187,11 @@ public final class ScheduledRecording {
return this;
}
+ public Builder setInputId(String inputId) {
+ mInputId = inputId;
+ return this;
+ }
+
public Builder setChannelId(long channelId) {
mChannelId = channelId;
return this;
@@ -121,18 +202,58 @@ public final class ScheduledRecording {
return this;
}
+ public Builder setProgramTitle(String programTitle) {
+ mProgramTitle = programTitle;
+ return this;
+ }
+
private Builder setType(@RecordingType int type) {
mType = type;
return this;
}
- public Builder setStartTime(long startTime) {
- mStartTime = startTime;
+ public Builder setStartTimeMs(long startTimeMs) {
+ mStartTimeMs = startTimeMs;
+ return this;
+ }
+
+ public Builder setEndTimeMs(long endTimeMs) {
+ mEndTimeMs = endTimeMs;
+ return this;
+ }
+
+ public Builder setSeasonNumber(String seasonNumber) {
+ mSeasonNumber = seasonNumber;
+ return this;
+ }
+
+ public Builder setEpisodeNumber(String episodeNumber) {
+ mEpisodeNumber = episodeNumber;
+ return this;
+ }
+
+ public Builder setEpisodeTitle(String episodeTitle) {
+ mEpisodeTitle = episodeTitle;
+ return this;
+ }
+
+ public Builder setProgramDescription(String description) {
+ mProgramDescription = description;
+ return this;
+ }
+
+ public Builder setProgramLongDescription(String longDescription) {
+ mProgramLongDescription = longDescription;
return this;
}
- public Builder setEndTime(long endTime) {
- mEndTime = endTime;
+ public Builder setProgramPosterArtUri(String programPosterArtUri) {
+ mProgramPosterArtUri = programPosterArtUri;
+ return this;
+ }
+
+ public Builder setProgramThumbnailUri(String programThumbnailUri) {
+ mProgramThumbnailUri = programThumbnailUri;
return this;
}
@@ -141,14 +262,16 @@ public final class ScheduledRecording {
return this;
}
- public Builder setParentSeasonRecording(SeasonRecording parentSeasonRecording) {
- mParentSeasonRecording = parentSeasonRecording;
+ public Builder setSeriesRecordingId(long seriesRecordingId) {
+ mSeriesRecordingId = seriesRecordingId;
return this;
}
public ScheduledRecording build() {
- return new ScheduledRecording(mId, mPriority, mChannelId, mProgramId, mType, mStartTime,
- mEndTime, mState, mParentSeasonRecording);
+ return new ScheduledRecording(mId, mPriority, mInputId, mChannelId, mProgramId,
+ mProgramTitle, mType, mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber,
+ mEpisodeTitle, mProgramDescription, mProgramLongDescription,
+ mProgramPosterArtUri, mProgramThumbnailUri, mState, mSeriesRecordingId);
}
}
@@ -157,22 +280,36 @@ public final class ScheduledRecording {
*/
public static Builder buildFrom(ScheduledRecording orig) {
return new Builder()
- .setId(orig.mId).setChannelId(orig.mChannelId)
- .setEndTime(orig.mEndTimeMs).setParentSeasonRecording(orig.mParentSeasonRecording)
+ .setId(orig.mId)
+ .setInputId(orig.mInputId)
+ .setChannelId(orig.mChannelId)
+ .setEndTimeMs(orig.mEndTimeMs)
+ .setSeriesRecordingId(orig.mSeriesRecordingId)
.setProgramId(orig.mProgramId)
- .setStartTime(orig.mStartTimeMs).setState(orig.mState).setType(orig.mType);
+ .setProgramTitle(orig.mProgramTitle)
+ .setStartTimeMs(orig.mStartTimeMs)
+ .setSeasonNumber(orig.getSeasonNumber())
+ .setEpisodeNumber(orig.getEpisodeNumber())
+ .setEpisodeTitle(orig.getEpisodeTitle())
+ .setProgramDescription(orig.getProgramDescription())
+ .setProgramLongDescription(orig.getProgramLongDescription())
+ .setProgramPosterArtUri(orig.getProgramPosterArtUri())
+ .setProgramThumbnailUri(orig.getProgramThumbnailUri())
+ .setState(orig.mState).setType(orig.mType);
}
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS,
- STATE_RECORDING_UNEXPECTEDLY_STOPPED, STATE_RECORDING_FINISHED, STATE_RECORDING_FAILED})
+ @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS, STATE_RECORDING_FINISHED,
+ STATE_RECORDING_FAILED, STATE_RECORDING_CLIPPED, STATE_RECORDING_DELETED,
+ STATE_RECORDING_CANCELED})
public @interface RecordingState {}
public static final int STATE_RECORDING_NOT_STARTED = 0;
public static final int STATE_RECORDING_IN_PROGRESS = 1;
- @Deprecated // It is not used.
- public static final int STATE_RECORDING_UNEXPECTEDLY_STOPPED = 2;
- public static final int STATE_RECORDING_FINISHED = 3;
- public static final int STATE_RECORDING_FAILED = 4;
+ public static final int STATE_RECORDING_FINISHED = 2;
+ public static final int STATE_RECORDING_FAILED = 3;
+ public static final int STATE_RECORDING_CLIPPED = 4;
+ public static final int STATE_RECORDING_DELETED = 5;
+ public static final int STATE_RECORDING_CANCELED = 6;
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_TIMED, TYPE_PROGRAM})
@@ -180,27 +317,39 @@ public final class ScheduledRecording {
/**
* Record with given time range.
*/
- static final int TYPE_TIMED = 1;
+ public static final int TYPE_TIMED = 1;
/**
* Record with a given program.
*/
- static final int TYPE_PROGRAM = 2;
+ public static final int TYPE_PROGRAM = 2;
@RecordingType private final int mType;
/**
- * Use this projection if you want to create {@link ScheduledRecording} object using {@link #fromCursor}.
+ * Use this projection if you want to create {@link ScheduledRecording} object using
+ * {@link #fromCursor}.
*/
public static final String[] PROJECTION = {
- // Columns must match what is read in Recording.fromCursor()
- DvrContract.Recordings._ID,
- DvrContract.Recordings.COLUMN_PRIORITY,
- DvrContract.Recordings.COLUMN_TYPE,
- DvrContract.Recordings.COLUMN_CHANNEL_ID,
- DvrContract.Recordings.COLUMN_PROGRAM_ID,
- DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS,
- DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS,
- DvrContract.Recordings.COLUMN_STATE};
+ // Columns must match what is read in #fromCursor
+ Schedules._ID,
+ Schedules.COLUMN_PRIORITY,
+ Schedules.COLUMN_TYPE,
+ Schedules.COLUMN_INPUT_ID,
+ Schedules.COLUMN_CHANNEL_ID,
+ Schedules.COLUMN_PROGRAM_ID,
+ Schedules.COLUMN_PROGRAM_TITLE,
+ Schedules.COLUMN_START_TIME_UTC_MILLIS,
+ Schedules.COLUMN_END_TIME_UTC_MILLIS,
+ Schedules.COLUMN_SEASON_NUMBER,
+ Schedules.COLUMN_EPISODE_NUMBER,
+ Schedules.COLUMN_EPISODE_TITLE,
+ Schedules.COLUMN_PROGRAM_DESCRIPTION,
+ Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION,
+ Schedules.COLUMN_PROGRAM_POST_ART_URI,
+ Schedules.COLUMN_PROGRAM_THUMBNAIL_URI,
+ Schedules.COLUMN_STATE,
+ Schedules.COLUMN_SERIES_RECORDING_ID};
+
/**
* Creates {@link ScheduledRecording} object from the given {@link Cursor}.
*/
@@ -210,65 +359,145 @@ public final class ScheduledRecording {
.setId(c.getLong(++index))
.setPriority(c.getLong(++index))
.setType(recordingType(c.getString(++index)))
+ .setInputId(c.getString(++index))
.setChannelId(c.getLong(++index))
.setProgramId(c.getLong(++index))
- .setStartTime(c.getLong(++index))
- .setEndTime(c.getLong(++index))
+ .setProgramTitle(c.getString(++index))
+ .setStartTimeMs(c.getLong(++index))
+ .setEndTimeMs(c.getLong(++index))
+ .setSeasonNumber(c.getString(++index))
+ .setEpisodeNumber(c.getString(++index))
+ .setEpisodeTitle(c.getString(++index))
+ .setProgramDescription(c.getString(++index))
+ .setProgramLongDescription(c.getString(++index))
+ .setProgramPosterArtUri(c.getString(++index))
+ .setProgramThumbnailUri(c.getString(++index))
.setState(recordingState(c.getString(++index)))
+ .setSeriesRecordingId(c.getLong(++index))
.build();
}
public static ContentValues toContentValues(ScheduledRecording r) {
ContentValues values = new ContentValues();
- values.put(DvrContract.Recordings.COLUMN_CHANNEL_ID, r.getChannelId());
- values.put(DvrContract.Recordings.COLUMN_PROGRAM_ID, r.getProgramId());
- values.put(DvrContract.Recordings.COLUMN_PRIORITY, r.getPriority());
- values.put(DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
- values.put(DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
- values.put(DvrContract.Recordings.COLUMN_STATE, r.getState());
- values.put(DvrContract.Recordings.COLUMN_TYPE, r.getType());
+ if (r.getId() != ID_NOT_SET) {
+ values.put(Schedules._ID, r.getId());
+ }
+ values.put(Schedules.COLUMN_INPUT_ID, r.getInputId());
+ values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId());
+ values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId());
+ values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle());
+ values.put(Schedules.COLUMN_PRIORITY, r.getPriority());
+ values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
+ values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
+ values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber());
+ values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber());
+ values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle());
+ values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription());
+ values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription());
+ values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri());
+ values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri());
+ values.put(Schedules.COLUMN_STATE, recordingState(r.getState()));
+ values.put(Schedules.COLUMN_TYPE, recordingType(r.getType()));
+ if (r.getSeriesRecordingId() != ID_NOT_SET) {
+ values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId());
+ } else {
+ values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID);
+ }
return values;
}
+ public static ScheduledRecording fromParcel(Parcel in) {
+ return new Builder()
+ .setId(in.readLong())
+ .setPriority(in.readLong())
+ .setInputId(in.readString())
+ .setChannelId(in.readLong())
+ .setProgramId(in.readLong())
+ .setProgramTitle(in.readString())
+ .setType(in.readInt())
+ .setStartTimeMs(in.readLong())
+ .setEndTimeMs(in.readLong())
+ .setSeasonNumber(in.readString())
+ .setEpisodeNumber(in.readString())
+ .setEpisodeTitle(in.readString())
+ .setProgramDescription(in.readString())
+ .setProgramLongDescription(in.readString())
+ .setProgramPosterArtUri(in.readString())
+ .setProgramThumbnailUri(in.readString())
+ .setState(in.readInt())
+ .setSeriesRecordingId(in.readLong())
+ .build();
+ }
+
+ public static final Parcelable.Creator<ScheduledRecording> CREATOR =
+ new Parcelable.Creator<ScheduledRecording>() {
+ @Override
+ public ScheduledRecording createFromParcel(Parcel in) {
+ return ScheduledRecording.fromParcel(in);
+ }
+
+ @Override
+ public ScheduledRecording[] newArray(int size) {
+ return new ScheduledRecording[size];
+ }
+ };
+
/**
* The ID internal to Live TV
*/
- private final long mId;
+ private long mId;
/**
* The priority of this recording.
*
- * <p> The lowest number is recorded first. If there is a tie in priority then the lower id
+ * <p> The highest number is recorded first. If there is a tie in priority then the higher id
* wins.
*/
private final long mPriority;
-
+ private final String mInputId;
private final long mChannelId;
/**
* Optional id of the associated program.
- *
*/
private final long mProgramId;
+ private final String mProgramTitle;
private final long mStartTimeMs;
private final long mEndTimeMs;
+ private final String mSeasonNumber;
+ private final String mEpisodeNumber;
+ private final String mEpisodeTitle;
+ private final String mProgramDescription;
+ private final String mProgramLongDescription;
+ private final String mProgramPosterArtUri;
+ private final String mProgramThumbnailUri;
@RecordingState private final int mState;
+ private final long mSeriesRecordingId;
- private final SeasonRecording mParentSeasonRecording;
-
- private ScheduledRecording(long id, long priority, long channelId, long programId,
- @RecordingType int type, long startTime, long endTime,
- @RecordingState int state, SeasonRecording parentSeasonRecording) {
+ private ScheduledRecording(long id, long priority, String inputId, long channelId, long programId,
+ String programTitle, @RecordingType int type, long startTime, long endTime,
+ String seasonNumber, String episodeNumber, String episodeTitle,
+ String programDescription, String programLongDescription, String programPosterArtUri,
+ String programThumbnailUri, @RecordingState int state, long seriesRecordingId) {
mId = id;
mPriority = priority;
+ mInputId = inputId;
mChannelId = channelId;
mProgramId = programId;
+ mProgramTitle = programTitle;
mType = type;
mStartTimeMs = startTime;
mEndTimeMs = endTime;
+ mSeasonNumber = seasonNumber;
+ mEpisodeNumber = episodeNumber;
+ mEpisodeTitle = episodeTitle;
+ mProgramDescription = programDescription;
+ mProgramLongDescription = programLongDescription;
+ mProgramPosterArtUri = programPosterArtUri;
+ mProgramThumbnailUri = programThumbnailUri;
mState = state;
- mParentSeasonRecording = parentSeasonRecording;
+ mSeriesRecordingId = seriesRecordingId;
}
/**
@@ -281,6 +510,13 @@ public final class ScheduledRecording {
}
/**
+ * Returns schedules' input id.
+ */
+ public String getInputId() {
+ return mInputId;
+ }
+
+ /**
* Returns recorded {@link Channel}.
*/
public long getChannelId() {
@@ -295,6 +531,13 @@ public final class ScheduledRecording {
}
/**
+ * Return the optional program Title
+ */
+ public String getProgramTitle() {
+ return mProgramTitle;
+ }
+
+ /**
* Returns started time.
*/
public long getStartTimeMs() {
@@ -309,6 +552,55 @@ public final class ScheduledRecording {
}
/**
+ * Returns the season number.
+ */
+ public String getSeasonNumber() {
+ return mSeasonNumber;
+ }
+
+ /**
+ * Returns the episode number.
+ */
+ public String getEpisodeNumber() {
+ return mEpisodeNumber;
+ }
+
+ /**
+ * Returns the episode title.
+ */
+ public String getEpisodeTitle() {
+ return mEpisodeTitle;
+ }
+
+ /**
+ * Returns the description of program.
+ */
+ public String getProgramDescription() {
+ return mProgramDescription;
+ }
+
+ /**
+ * Returns the long description of program.
+ */
+ public String getProgramLongDescription() {
+ return mProgramLongDescription;
+ }
+
+ /**
+ * Returns the poster uri of program.
+ */
+ public String getProgramPosterArtUri() {
+ return mProgramPosterArtUri;
+ }
+
+ /**
+ * Returns the thumb nail uri of program.
+ */
+ public String getProgramThumbnailUri() {
+ return mProgramThumbnailUri;
+ }
+
+ /**
* Returns duration.
*/
public long getDuration() {
@@ -316,43 +608,83 @@ public final class ScheduledRecording {
}
/**
- * Returns the state. The possible states are {@link #STATE_RECORDING_FINISHED},
- * {@link #STATE_RECORDING_IN_PROGRESS} and {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED}.
+ * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED},
+ * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED},
+ * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and
+ * {@link #STATE_RECORDING_DELETED}.
*/
@RecordingState public int getState() {
return mState;
}
/**
- * Returns {@link SeasonRecording} including this schedule.
+ * Returns the ID of the {@link SeriesRecording} including this schedule.
*/
- public SeasonRecording getParentSeasonRecording() {
- return mParentSeasonRecording;
+ public long getSeriesRecordingId() {
+ return mSeriesRecordingId;
}
public long getId() {
return mId;
}
+ /**
+ * Sets the ID;
+ */
+ public void setId(long id) {
+ mId = id;
+ }
+
public long getPriority() {
return mPriority;
}
/**
+ * Returns season number, episode number and episode title for display.
+ */
+ public String getEpisodeDisplayTitle(Context context) {
+ if (!TextUtils.isEmpty(mEpisodeNumber)) {
+ String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle;
+ if (TextUtils.equals(mSeasonNumber, "0")) {
+ // Do not show "S0: ".
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format_no_season_number),
+ mEpisodeNumber, episodeTitle);
+ } else {
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format),
+ mSeasonNumber, mEpisodeNumber, episodeTitle);
+ }
+ }
+ return mEpisodeTitle;
+ }
+
+ /**
+ * Returns the program's title withe its season and episode number.
+ */
+ public String getProgramTitleWithEpisodeNumber(Context context) {
+ if (TextUtils.isEmpty(mProgramTitle)) {
+ return mProgramTitle;
+ }
+ if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) {
+ return TextUtils.isEmpty(mEpisodeNumber) ? mProgramTitle : context.getString(
+ R.string.program_title_with_episode_number_no_season, mProgramTitle,
+ mEpisodeNumber);
+ } else {
+ return context.getString(R.string.program_title_with_episode_number, mProgramTitle,
+ mSeasonNumber, mEpisodeNumber);
+ }
+ }
+
+
+ /**
* Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}.
*/
private static @RecordingType int recordingType(String type) {
- int t;
- try {
- t = Integer.valueOf(type);
- } catch (NullPointerException | NumberFormatException e) {
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
- return TYPE_TIMED;
- }
- switch (t) {
- case TYPE_TIMED:
+ switch (type) {
+ case Schedules.TYPE_TIMED:
return TYPE_TIMED;
- case TYPE_PROGRAM:
+ case Schedules.TYPE_PROGRAM:
return TYPE_PROGRAM;
default:
SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
@@ -361,28 +693,40 @@ public final class ScheduledRecording {
}
/**
+ * Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}.
+ */
+ private static String recordingType(@RecordingType int type) {
+ switch (type) {
+ case TYPE_TIMED:
+ return Schedules.TYPE_TIMED;
+ case TYPE_PROGRAM:
+ return Schedules.TYPE_PROGRAM;
+ default:
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
+ return Schedules.TYPE_TIMED;
+ }
+ }
+
+ /**
* Converts a string to a @RecordingState int, defaulting to
* {@link #STATE_RECORDING_NOT_STARTED}.
*/
private static @RecordingState int recordingState(String state) {
- int s;
- try {
- s = Integer.valueOf(state);
- } catch (NullPointerException | NumberFormatException e) {
- SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
- return STATE_RECORDING_NOT_STARTED;
- }
- switch (s) {
- case STATE_RECORDING_NOT_STARTED:
+ switch (state) {
+ case Schedules.STATE_RECORDING_NOT_STARTED:
return STATE_RECORDING_NOT_STARTED;
- case STATE_RECORDING_IN_PROGRESS:
+ case Schedules.STATE_RECORDING_IN_PROGRESS:
return STATE_RECORDING_IN_PROGRESS;
- case STATE_RECORDING_FINISHED:
+ case Schedules.STATE_RECORDING_FINISHED:
return STATE_RECORDING_FINISHED;
- case STATE_RECORDING_UNEXPECTEDLY_STOPPED:
- return STATE_RECORDING_UNEXPECTEDLY_STOPPED;
- case STATE_RECORDING_FAILED:
+ case Schedules.STATE_RECORDING_FAILED:
return STATE_RECORDING_FAILED;
+ case Schedules.STATE_RECORDING_CLIPPED:
+ return STATE_RECORDING_CLIPPED;
+ case Schedules.STATE_RECORDING_DELETED:
+ return STATE_RECORDING_DELETED;
+ case Schedules.STATE_RECORDING_CANCELED:
+ return STATE_RECORDING_CANCELED;
default:
SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
return STATE_RECORDING_NOT_STARTED;
@@ -390,20 +734,140 @@ public final class ScheduledRecording {
}
/**
+ * Converts a @RecordingState int to string, defaulting to
+ * {@link Schedules#STATE_RECORDING_NOT_STARTED}.
+ */
+ private static String recordingState(@RecordingState int state) {
+ switch (state) {
+ case STATE_RECORDING_NOT_STARTED:
+ return Schedules.STATE_RECORDING_NOT_STARTED;
+ case STATE_RECORDING_IN_PROGRESS:
+ return Schedules.STATE_RECORDING_IN_PROGRESS;
+ case STATE_RECORDING_FINISHED:
+ return Schedules.STATE_RECORDING_FINISHED;
+ case STATE_RECORDING_FAILED:
+ return Schedules.STATE_RECORDING_FAILED;
+ case STATE_RECORDING_CLIPPED:
+ return Schedules.STATE_RECORDING_CLIPPED;
+ case STATE_RECORDING_DELETED:
+ return Schedules.STATE_RECORDING_DELETED;
+ case STATE_RECORDING_CANCELED:
+ return Schedules.STATE_RECORDING_CANCELED;
+ default:
+ SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
+ return Schedules.STATE_RECORDING_NOT_STARTED;
+ }
+ }
+
+ /**
* Checks if the {@code period} overlaps with the recording time.
*/
public boolean isOverLapping(Range<Long> period) {
- return mStartTimeMs <= period.getUpper() && mEndTimeMs >= period.getLower();
+ return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower();
}
@Override
public String toString() {
return "ScheduledRecording[" + mId
+ "]"
- + "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
+ + "(inputId=" + mInputId
+ + ",channelId=" + mChannelId
+ + ",programId=" + mProgramId
+ + ",programTitle=" + mProgramTitle
+ + ",type=" + mType
+ + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
+ ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs)
+ + ",seasonNumber=" + mSeasonNumber
+ + ",episodeNumber=" + mEpisodeNumber
+ + ",episodeTitle=" + mEpisodeTitle
+ + ",programDescription=" + mProgramDescription
+ + ",programLongDescription=" + mProgramLongDescription
+ + ",programPosterArtUri=" + mProgramPosterArtUri
+ + ",programThumbnailUri=" + mProgramThumbnailUri
+ ",state=" + mState
+ ",priority=" + mPriority
+ + ",seriesRecordingId=" + mSeriesRecordingId
+ ")";
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int paramInt) {
+ out.writeLong(mId);
+ out.writeLong(mPriority);
+ out.writeString(mInputId);
+ out.writeLong(mChannelId);
+ out.writeLong(mProgramId);
+ out.writeString(mProgramTitle);
+ out.writeInt(mType);
+ out.writeLong(mStartTimeMs);
+ out.writeLong(mEndTimeMs);
+ out.writeString(mSeasonNumber);
+ out.writeString(mEpisodeNumber);
+ out.writeString(mEpisodeTitle);
+ out.writeString(mProgramDescription);
+ out.writeString(mProgramLongDescription);
+ out.writeString(mProgramPosterArtUri);
+ out.writeString(mProgramThumbnailUri);
+ out.writeInt(mState);
+ out.writeLong(mSeriesRecordingId);
+ }
+
+ /**
+ * Returns {@code true} if the recording is not started yet, otherwise @{code false}.
+ */
+ public boolean isNotStarted() {
+ return mState == STATE_RECORDING_NOT_STARTED;
+ }
+
+ /**
+ * Returns {@code true} if the recording is in progress, otherwise @{code false}.
+ */
+ public boolean isInProgress() {
+ return mState == STATE_RECORDING_IN_PROGRESS;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ScheduledRecording)) {
+ return false;
+ }
+ ScheduledRecording r = (ScheduledRecording) obj;
+ return mId == r.mId
+ && mPriority == r.mPriority
+ && mChannelId == r.mChannelId
+ && mProgramId == r.mProgramId
+ && Objects.equals(mProgramTitle, r.mProgramTitle)
+ && mType == r.mType
+ && mStartTimeMs == r.mStartTimeMs
+ && mEndTimeMs == r.mEndTimeMs
+ && Objects.equals(mSeasonNumber, r.mSeasonNumber)
+ && Objects.equals(mEpisodeNumber, r.mEpisodeNumber)
+ && Objects.equals(mEpisodeTitle, r.mEpisodeTitle)
+ && Objects.equals(mProgramDescription, r.getProgramDescription())
+ && Objects.equals(mProgramLongDescription, r.getProgramLongDescription())
+ && Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri())
+ && Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri())
+ && mState == r.mState
+ && mSeriesRecordingId == r.mSeriesRecordingId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mPriority, mChannelId, mProgramId, mProgramTitle, mType,
+ mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, mEpisodeTitle,
+ mProgramDescription, mProgramLongDescription, mProgramPosterArtUri,
+ mProgramThumbnailUri, mState, mSeriesRecordingId);
+ }
+
+ /**
+ * Returns an array containing all of the elements in the list.
+ */
+ public static ScheduledRecording[] toArray(Collection<ScheduledRecording> schedules) {
+ return schedules.toArray(new ScheduledRecording[schedules.size()]);
+ }
}
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java
index ff9bde68..25904ee4 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/Scheduler.java
@@ -20,86 +20,118 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.os.Handler;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Looper;
-import android.os.Message;
+import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
import android.util.Log;
-import android.util.LongSparseArray;
import android.util.Range;
-import com.android.tv.data.Channel;
+import com.android.tv.InputSessionManager;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelDataManager.Listener;
+import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.util.Clock;
+import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
+import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* The core class to manage schedule and run actual recording.
*/
-@VisibleForTesting
-public class Scheduler implements DvrDataManager.ScheduledRecordingListener {
+@MainThread
+public class Scheduler extends TvInputCallback implements ScheduledRecordingListener {
private static final String TAG = "Scheduler";
private static final boolean DEBUG = false;
private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5);
@VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1);
- /**
- * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
- */
- public final class HandlerWrapper extends Handler {
- public static final int MESSAGE_REMOVE = 999;
- private final long mId;
-
- HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, RecordingTask recordingTask) {
- super(looper, recordingTask);
- mId = scheduledRecording.getId();
- }
-
- @Override
- public void handleMessage(Message msg) {
- // The RecordingTask gets a chance first.
- // It must return false to pass this message to here.
- if (msg.what == MESSAGE_REMOVE) {
- if (DEBUG) Log.d(TAG, "done " + mId);
- mPendingRecordings.remove(mId);
- }
- removeCallbacksAndMessages(null);
- super.handleMessage(msg);
- }
- }
-
- private final LongSparseArray<HandlerWrapper> mPendingRecordings = new LongSparseArray<>();
private final Looper mLooper;
- private final DvrSessionManager mSessionManager;
+ private final InputSessionManager mSessionManager;
private final WritableDvrDataManager mDataManager;
private final DvrManager mDvrManager;
private final ChannelDataManager mChannelDataManager;
+ private final TvInputManagerHelper mInputManager;
private final Context mContext;
private final Clock mClock;
private final AlarmManager mAlarmManager;
- public Scheduler(Looper looper, DvrManager dvrManager, DvrSessionManager sessionManager,
+ private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>();
+ private long mLastStartTimePendingMs;
+
+ public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager,
WritableDvrDataManager dataManager, ChannelDataManager channelDataManager,
- Context context, Clock clock,
+ TvInputManagerHelper inputManager, Context context, Clock clock,
AlarmManager alarmManager) {
mLooper = looper;
mDvrManager = dvrManager;
mSessionManager = sessionManager;
mDataManager = dataManager;
mChannelDataManager = channelDataManager;
+ mInputManager = inputManager;
mContext = context;
mClock = clock;
mAlarmManager = alarmManager;
}
+ /**
+ * Starts the scheduler.
+ */
+ public void start() {
+ mDataManager.addScheduledRecordingListener(this);
+ mInputManager.addCallback(this);
+ if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
+ updateInternal();
+ } else {
+ if (!mDataManager.isDvrScheduleLoadFinished()) {
+ mDataManager.addDvrScheduleLoadFinishedListener(
+ new OnDvrScheduleLoadFinishedListener() {
+ @Override
+ public void onDvrScheduleLoadFinished() {
+ mDataManager.removeDvrScheduleLoadFinishedListener(this);
+ updateInternal();
+ }
+ });
+ }
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(new Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ updateInternal();
+ }
+
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ }
+ }
+ }
+
+ /**
+ * Stops the scheduler.
+ */
+ public void stop() {
+ mInputManager.removeCallback(this);
+ mDataManager.removeScheduledRecordingListener(this);
+ }
+
private void updatePendingRecordings() {
- List<ScheduledRecording> scheduledRecordings = mDataManager.getRecordingsThatOverlapWith(
- new Range(mClock.currentTimeMillis(),
- mClock.currentTimeMillis() + SOON_DURATION_IN_MS));
- // TODO(DVR): handle removing and updating exiting recordings.
+ List<ScheduledRecording> scheduledRecordings = mDataManager
+ .getScheduledRecordings(new Range<>(mLastStartTimePendingMs,
+ mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
+ ScheduledRecording.STATE_RECORDING_NOT_STARTED);
for (ScheduledRecording r : scheduledRecordings) {
scheduleRecordingSoon(r);
}
@@ -110,70 +142,150 @@ public class Scheduler implements DvrDataManager.ScheduledRecordingListener {
*/
public void update() {
if (DEBUG) Log.d(TAG, "update");
- updatePendingRecordings();
- updateNextAlarm();
+ updateInternal();
}
- @Override
- public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
- if (DEBUG) Log.d(TAG, "added " + scheduledRecording);
- if (startsWithin(scheduledRecording, SOON_DURATION_IN_MS)) {
- scheduleRecordingSoon(scheduledRecording);
- } else {
+ private void updateInternal() {
+ if (isInitialized()) {
+ updatePendingRecordings();
updateNextAlarm();
}
}
+ private boolean isInitialized() {
+ return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished();
+ }
+
@Override
- public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
- long id = scheduledRecording.getId();
- HandlerWrapper wrapper = mPendingRecordings.get(id);
- if (wrapper != null) {
- wrapper.removeCallbacksAndMessages(null);
- mPendingRecordings.remove(id);
- } else {
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
+ if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules));
+ if (!isInitialized()) {
+ return;
+ }
+ handleScheduleChange(schedules);
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules));
+ if (!isInitialized()) {
+ return;
+ }
+ 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());
+ if (scheduler != null) {
+ scheduler.removeSchedule(schedule);
+ needToUpdateAlarm = true;
+ }
+ }
+ if (needToUpdateAlarm) {
updateNextAlarm();
}
}
@Override
- public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
- //TODO(DVR): implement
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules));
+ if (!isInitialized()) {
+ return;
+ }
+ // 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());
+ if (scheduler != null) {
+ scheduler.updateSchedule(schedule);
+ }
+ }
+ handleScheduleChange(schedules);
}
- private void scheduleRecordingSoon(ScheduledRecording scheduledRecording) {
- Channel channel = mChannelDataManager.getChannel(scheduledRecording.getChannelId());
- RecordingTask recordingTask = new RecordingTask(scheduledRecording, channel, mDvrManager,
- mSessionManager, mDataManager, mClock);
- HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, scheduledRecording,
- recordingTask);
- recordingTask.setHandler(handlerWrapper);
- mPendingRecordings.put(scheduledRecording.getId(), handlerWrapper);
- handlerWrapper.sendEmptyMessage(RecordingTask.MESSAGE_INIT);
+ private void handleScheduleChange(ScheduledRecording... schedules) {
+ boolean needToUpdateAlarm = false;
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ if (startsWithin(schedule, SOON_DURATION_IN_MS)) {
+ scheduleRecordingSoon(schedule);
+ } else {
+ needToUpdateAlarm = true;
+ }
+ }
+ }
+ if (needToUpdateAlarm) {
+ updateNextAlarm();
+ }
+ }
+
+ private void scheduleRecordingSoon(ScheduledRecording schedule) {
+ 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);
+ return;
+ }
+ if (!input.canRecord() || input.getTunerCount() <= 0) {
+ Log.e(TAG, "TV input doesn't support recording: " + input);
+ mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
+ return;
+ }
+ InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
+ if (scheduler == null) {
+ scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager,
+ mDvrManager, mDataManager, mSessionManager, mClock);
+ mInputSchedulerMap.put(input.getId(), scheduler);
+ }
+ scheduler.addSchedule(schedule);
+ if (mLastStartTimePendingMs < schedule.getStartTimeMs()) {
+ mLastStartTimePendingMs = schedule.getStartTimeMs();
+ }
}
private void updateNextAlarm() {
- long lastStartTimePending = getLastStartTimePending();
- long nextStartTime = mDataManager.getNextScheduledStartTimeAfter(lastStartTimePending);
+ long nextStartTime = mDataManager.getNextScheduledStartTimeAfter(
+ Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis()));
if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) {
long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START;
if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt);
Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
- //This will cancel the previous alarm.
+ // This will cancel the previous alarm.
mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
} else {
if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
}
}
- private long getLastStartTimePending() {
- // TODO(DVR): implement
- return mClock.currentTimeMillis();
- }
-
@VisibleForTesting
boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) {
return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs;
}
+
+ // No need to remove input task scheduler when the input is removed. If the input is removed
+ // temporarily, the scheduler should keep the non-started schedules.
+ @Override
+ public void onInputUpdated(String inputId) {
+ InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId);
+ if (scheduler != null) {
+ scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId));
+ }
+ }
+
+ @Override
+ public void onTvInputInfoUpdated(TvInputInfo input) {
+ InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
+ if (scheduler != null) {
+ scheduler.updateTvInputInfo(input);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/SeasonRecording.java b/src/com/android/tv/dvr/SeasonRecording.java
deleted file mode 100644
index 7f89e135..00000000
--- a/src/com/android/tv/dvr/SeasonRecording.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2015 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 java.util.List;
-
-/**
- * A data class for one recorded contents.
- */
-public class SeasonRecording {
- private static final String TAG = "Recording";
-
- /**
- * Constant for all season.
- */
- private static final int ALL_SEASON = -1;
-
- private List<ScheduledRecording> mSchedule;
- private String mTitle;
- private int mSeasonNumber;
-}
diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/SeriesInfo.java
new file mode 100644
index 00000000..30256dc5
--- /dev/null
+++ b/src/com/android/tv/dvr/SeriesInfo.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * Series information.
+ */
+public class SeriesInfo {
+ private final String mId;
+ private final String mTitle;
+ private final String mDescription;
+ private final String mLongDescription;
+ private final int[] mCanonicalGenreIds;
+ private final String mPosterUri;
+ private final String mPhotoUri;
+
+ public SeriesInfo(String id, String title, String description, String longDescription,
+ int[] canonicalGenreIds, String posterUri, String photoUri) {
+ this.mId = id;
+ this.mTitle = title;
+ this.mDescription = description;
+ this.mLongDescription = longDescription;
+ this.mCanonicalGenreIds = canonicalGenreIds;
+ this.mPosterUri = posterUri;
+ this.mPhotoUri = photoUri;
+ }
+
+ /** Returns the ID. **/
+ public String getId() {
+ return mId;
+ }
+
+ /** Returns the title. **/
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Returns the description. **/
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /** Returns the description. **/
+ public String getLongDescription() {
+ return mLongDescription;
+ }
+
+ /** Returns the canonical genre IDs. **/
+ public int[] getCanonicalGenreIds() {
+ return mCanonicalGenreIds;
+ }
+
+ /** Returns the poster URI. **/
+ public String getPosterUri() {
+ return mPosterUri;
+ }
+
+ /** Returns the photo URI. **/
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+}
diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java
new file mode 100644
index 00000000..fc68eaf7
--- /dev/null
+++ b/src/com/android/tv/dvr/SeriesRecording.java
@@ -0,0 +1,749 @@
+/*
+ * Copyright (C) 2015 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.ContentValues;
+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.Program;
+import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
+import com.android.tv.util.Utils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * Schedules the recording of a Series of Programs.
+ *
+ * <p>
+ * Contains the data needed to create new ScheduleRecordings as the programs become available in
+ * the EPG.
+ */
+public class SeriesRecording implements Parcelable {
+ /**
+ * Indicates that the ID is not assigned yet.
+ */
+ public static final long ID_NOT_SET = 0;
+
+ /**
+ * The default priority of this recording.
+ */
+ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL})
+ public @interface ChannelOption {}
+ /**
+ * An option which indicates that the episodes in one channel are recorded.
+ */
+ public static final int OPTION_CHANNEL_ONE = 0;
+ /**
+ * An option which indicates that the episodes in all the channels are recorded.
+ */
+ public static final int OPTION_CHANNEL_ALL = 1;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true,
+ value = {STATE_SERIES_NORMAL, STATE_SERIES_CANCELED})
+ private @interface SeriesState {}
+
+ /**
+ * The state indicates that the series recording is a normal one.
+ */
+ public static final int STATE_SERIES_NORMAL = 0;
+
+ /**
+ * The state indicates that the series recording is canceled.
+ */
+ public static final int STATE_SERIES_CANCELED = 1;
+
+ /**
+ * Compare priority in descending order.
+ */
+ public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR =
+ new Comparator<SeriesRecording>() {
+ @Override
+ public int compare(SeriesRecording lhs, SeriesRecording 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;
+ }
+ };
+
+ /**
+ * Compare ID in ascending order.
+ */
+ public static final Comparator<SeriesRecording> ID_COMPARATOR =
+ new Comparator<SeriesRecording>() {
+ @Override
+ public int compare(SeriesRecording lhs, SeriesRecording rhs) {
+ return Long.compare(lhs.mId, rhs.mId);
+ }
+ };
+
+ /**
+ * Creates a new Builder with the values set from the series information of {@link Program}.
+ */
+ public static Builder builder(String inputId, Program p) {
+ return new Builder()
+ .setInputId(inputId)
+ .setSeriesId(p.getSeriesId())
+ .setChannelId(p.getChannelId())
+ .setTitle(p.getTitle())
+ .setDescription(p.getDescription())
+ .setLongDescription(p.getLongDescription())
+ .setCanonicalGenreIds(p.getCanonicalGenreIds())
+ .setPosterUri(p.getPosterArtUri())
+ .setPhotoUri(p.getThumbnailUri());
+ }
+
+ /**
+ * Creates a new Builder with the values set from an existing {@link SeriesRecording}.
+ */
+ @VisibleForTesting
+ public static Builder buildFrom(SeriesRecording r) {
+ return new Builder()
+ .setId(r.mId)
+ .setInputId(r.getInputId())
+ .setChannelId(r.getChannelId())
+ .setPriority(r.getPriority())
+ .setTitle(r.getTitle())
+ .setDescription(r.getDescription())
+ .setLongDescription(r.getLongDescription())
+ .setSeriesId(r.getSeriesId())
+ .setStartFromEpisode(r.getStartFromEpisode())
+ .setStartFromSeason(r.getStartFromSeason())
+ .setChannelOption(r.getChannelOption())
+ .setCanonicalGenreIds(r.getCanonicalGenreIds())
+ .setPosterUri(r.getPosterUri())
+ .setPhotoUri(r.getPhotoUri())
+ .setState(r.getState());
+ }
+
+ /**
+ * Use this projection if you want to create {@link SeriesRecording} object using
+ * {@link #fromCursor}.
+ */
+ public static final String[] PROJECTION = {
+ // Columns must match what is read in fromCursor()
+ SeriesRecordings._ID,
+ SeriesRecordings.COLUMN_INPUT_ID,
+ SeriesRecordings.COLUMN_CHANNEL_ID,
+ SeriesRecordings.COLUMN_PRIORITY,
+ SeriesRecordings.COLUMN_TITLE,
+ SeriesRecordings.COLUMN_SHORT_DESCRIPTION,
+ SeriesRecordings.COLUMN_LONG_DESCRIPTION,
+ SeriesRecordings.COLUMN_SERIES_ID,
+ SeriesRecordings.COLUMN_START_FROM_EPISODE,
+ SeriesRecordings.COLUMN_START_FROM_SEASON,
+ SeriesRecordings.COLUMN_CHANNEL_OPTION,
+ SeriesRecordings.COLUMN_CANONICAL_GENRE,
+ SeriesRecordings.COLUMN_POSTER_URI,
+ SeriesRecordings.COLUMN_PHOTO_URI,
+ SeriesRecordings.COLUMN_STATE
+ };
+ /**
+ * Creates {@link SeriesRecording} object from the given {@link Cursor}.
+ */
+ public static SeriesRecording fromCursor(Cursor c) {
+ int index = -1;
+ return new Builder()
+ .setId(c.getLong(++index))
+ .setInputId(c.getString(++index))
+ .setChannelId(c.getLong(++index))
+ .setPriority(c.getLong(++index))
+ .setTitle(c.getString(++index))
+ .setDescription(c.getString(++index))
+ .setLongDescription(c.getString(++index))
+ .setSeriesId(c.getString(++index))
+ .setStartFromEpisode(c.getInt(++index))
+ .setStartFromSeason(c.getInt(++index))
+ .setChannelOption(channelOption(c.getString(++index)))
+ .setCanonicalGenreIds(c.getString(++index))
+ .setPosterUri(c.getString(++index))
+ .setPhotoUri(c.getString(++index))
+ .setState(seriesRecordingCanceled(c.getString(++index)))
+ .build();
+ }
+
+ /**
+ * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings}
+ * and the values from {@code r}.
+ */
+ public static ContentValues toContentValues(SeriesRecording r) {
+ ContentValues values = new ContentValues();
+ if (r.getId() != ID_NOT_SET) {
+ values.put(SeriesRecordings._ID, r.getId());
+ } else {
+ values.putNull(SeriesRecordings._ID);
+ }
+ values.put(SeriesRecordings.COLUMN_INPUT_ID, r.getInputId());
+ values.put(SeriesRecordings.COLUMN_CHANNEL_ID, r.getChannelId());
+ values.put(SeriesRecordings.COLUMN_PRIORITY, r.getPriority());
+ values.put(SeriesRecordings.COLUMN_TITLE, r.getTitle());
+ values.put(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, r.getDescription());
+ values.put(SeriesRecordings.COLUMN_LONG_DESCRIPTION, r.getLongDescription());
+ values.put(SeriesRecordings.COLUMN_SERIES_ID, r.getSeriesId());
+ values.put(SeriesRecordings.COLUMN_START_FROM_EPISODE, r.getStartFromEpisode());
+ values.put(SeriesRecordings.COLUMN_START_FROM_SEASON, r.getStartFromSeason());
+ values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION,
+ channelOption(r.getChannelOption()));
+ values.put(SeriesRecordings.COLUMN_CANONICAL_GENRE,
+ 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()));
+ return values;
+ }
+
+ private static String channelOption(@ChannelOption int option) {
+ switch (option) {
+ case OPTION_CHANNEL_ONE:
+ return SeriesRecordings.OPTION_CHANNEL_ONE;
+ case OPTION_CHANNEL_ALL:
+ return SeriesRecordings.OPTION_CHANNEL_ALL;
+ }
+ return SeriesRecordings.OPTION_CHANNEL_ONE;
+ }
+
+ @ChannelOption private static int channelOption(String option) {
+ switch (option) {
+ case SeriesRecordings.OPTION_CHANNEL_ONE:
+ return OPTION_CHANNEL_ONE;
+ case SeriesRecordings.OPTION_CHANNEL_ALL:
+ return OPTION_CHANNEL_ALL;
+ }
+ return OPTION_CHANNEL_ONE;
+ }
+
+ private static String seriesRecordingCanceled(@SeriesState int state) {
+ switch (state) {
+ case STATE_SERIES_NORMAL:
+ return SeriesRecordings.STATE_SERIES_NORMAL;
+ case STATE_SERIES_CANCELED:
+ return SeriesRecordings.STATE_SERIES_CANCELED;
+ }
+ return SeriesRecordings.STATE_SERIES_NORMAL;
+ }
+
+ @SeriesState private static int seriesRecordingCanceled(String state) {
+ switch (state) {
+ case SeriesRecordings.STATE_SERIES_NORMAL:
+ return STATE_SERIES_NORMAL;
+ case SeriesRecordings.STATE_SERIES_CANCELED:
+ return STATE_SERIES_CANCELED;
+ }
+ return STATE_SERIES_NORMAL;
+ }
+
+ /**
+ * Builder for {@link SeriesRecording}.
+ */
+ public static class Builder {
+ private long mId = ID_NOT_SET;
+ private long mPriority = DvrScheduleManager.DEFAULT_SERIES_PRIORITY;
+ private String mTitle;
+ private String mDescription;
+ private String mLongDescription;
+ private String mInputId;
+ private long mChannelId;
+ private String mSeriesId;
+ private int mStartFromSeason = SeriesRecordings.THE_BEGINNING;
+ private int mStartFromEpisode = SeriesRecordings.THE_BEGINNING;
+ private int mChannelOption = OPTION_CHANNEL_ONE;
+ private int[] mCanonicalGenreIds;
+ private String mPosterUri;
+ private String mPhotoUri;
+ private int mState = SeriesRecording.STATE_SERIES_NORMAL;
+
+ /**
+ * @see #getId()
+ */
+ public Builder setId(long id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * @see #getPriority() ()
+ */
+ public Builder setPriority(long priority) {
+ mPriority = priority;
+ return this;
+ }
+
+ /**
+ * @see #getTitle()
+ */
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * @see #getDescription()
+ */
+ public Builder setDescription(String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * @see #getLongDescription()
+ */
+ public Builder setLongDescription(String longDescription) {
+ mLongDescription = longDescription;
+ return this;
+ }
+
+ /**
+ * @see #getInputId()
+ */
+ public Builder setInputId(String inputId) {
+ mInputId = inputId;
+ return this;
+ }
+ /**
+ * @see #getChannelId()
+ */
+ public Builder setChannelId(long channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ /**
+ * @see #getSeriesId()
+ */
+ public Builder setSeriesId(String seriesId) {
+ mSeriesId = seriesId;
+ return this;
+ }
+
+ /**
+ * @see #getStartFromSeason()
+ */
+ public Builder setStartFromSeason(int startFromSeason) {
+ mStartFromSeason = startFromSeason;
+ return this;
+ }
+
+ /**
+ * @see #getChannelOption()
+ */
+ public Builder setChannelOption(@ChannelOption int option) {
+ mChannelOption = option;
+ return this;
+ }
+
+ /**
+ * @see #getStartFromEpisode()
+ */
+ public Builder setStartFromEpisode(int startFromEpisode) {
+ mStartFromEpisode = startFromEpisode;
+ return this;
+ }
+
+ /**
+ * @see #getCanonicalGenreIds()
+ */
+ public Builder setCanonicalGenreIds(String genres) {
+ mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres);
+ return this;
+ }
+
+ /**
+ * @see #getCanonicalGenreIds()
+ */
+ public Builder setCanonicalGenreIds(int[] canonicalGenreIds) {
+ mCanonicalGenreIds = canonicalGenreIds;
+ return this;
+ }
+
+ /**
+ * @see #getPosterUri()
+ */
+ public Builder setPosterUri(String posterUri) {
+ mPosterUri = posterUri;
+ return this;
+ }
+
+ /**
+ * @see #getPhotoUri()
+ */
+ public Builder setPhotoUri(String photoUri) {
+ mPhotoUri = photoUri;
+ return this;
+ }
+
+ /**
+ * @see #getState()
+ */
+ public Builder setState(@SeriesState int state) {
+ mState = state;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link SeriesRecording}.
+ */
+ public SeriesRecording build() {
+ return new SeriesRecording(mId, mPriority, mTitle, mDescription, mLongDescription,
+ mInputId, mChannelId, mSeriesId, mStartFromSeason, mStartFromEpisode,
+ mChannelOption, mCanonicalGenreIds, mPosterUri, mPhotoUri, mState);
+ }
+ }
+
+ public static SeriesRecording fromParcel(Parcel in) {
+ return new Builder()
+ .setId(in.readLong())
+ .setPriority(in.readLong())
+ .setTitle(in.readString())
+ .setDescription(in.readString())
+ .setLongDescription(in.readString())
+ .setInputId(in.readString())
+ .setChannelId(in.readLong())
+ .setSeriesId(in.readString())
+ .setStartFromSeason(in.readInt())
+ .setStartFromEpisode(in.readInt())
+ .setChannelOption(in.readInt())
+ .setCanonicalGenreIds(in.createIntArray())
+ .setPosterUri(in.readString())
+ .setPhotoUri(in.readString())
+ .setState(in.readInt())
+ .build();
+ }
+
+ public static final Parcelable.Creator<SeriesRecording> CREATOR =
+ new Parcelable.Creator<SeriesRecording>() {
+ @Override
+ public SeriesRecording createFromParcel(Parcel in) {
+ return SeriesRecording.fromParcel(in);
+ }
+
+ @Override
+ public SeriesRecording[] newArray(int size) {
+ return new SeriesRecording[size];
+ }
+ };
+
+ private long mId;
+ private final long mPriority;
+ private final String mTitle;
+ private final String mDescription;
+ private final String mLongDescription;
+ private final String mInputId;
+ private final long mChannelId;
+ private final String mSeriesId;
+ private final int mStartFromSeason;
+ private final int mStartFromEpisode;
+ @ChannelOption private final int mChannelOption;
+ private final int[] mCanonicalGenreIds;
+ private final String mPosterUri;
+ private final String mPhotoUri;
+ @SeriesState private int mState;
+
+ /**
+ * The input id of this SeriesRecording.
+ */
+ public String getInputId() {
+ return mInputId;
+ }
+
+ /**
+ * The channelId to match.
+ */
+ public long getChannelId() {
+ return mChannelId;
+ }
+
+ /**
+ * The id of this SeriesRecording.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Sets the ID.
+ */
+ public void setId(long id) {
+ mId = id;
+ }
+
+ /**
+ * The priority of this recording.
+ *
+ * <p> The highest number is recorded first. If there is a tie in mPriority then the higher mId
+ * wins.
+ */
+ public long getPriority() {
+ return mPriority;
+ }
+
+ /**
+ * The series title.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * The series description.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * The long series description.
+ */
+ public String getLongDescription() {
+ return mLongDescription;
+ }
+
+ /**
+ * SeriesId when not null is used to match programs instead of using title and channelId.
+ *
+ * <p>SeriesId is an opaque but stable string.
+ */
+ @NonNull
+ public String getSeriesId() {
+ return mSeriesId;
+ }
+
+ /**
+ * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then
+ * only record episodes with a episodeNumber >= this
+ */
+ public int getStartFromEpisode() {
+ return mStartFromEpisode;
+ }
+
+ /**
+ * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a
+ * seasonNumber >= this
+ */
+ public int getStartFromSeason() {
+ return mStartFromSeason;
+ }
+
+ /**
+ * Returns the channel recording option.
+ */
+ @ChannelOption public int getChannelOption() {
+ return mChannelOption;
+ }
+
+ /**
+ * Returns the canonical genre ID's.
+ */
+ public int[] getCanonicalGenreIds() {
+ return mCanonicalGenreIds;
+ }
+
+ /**
+ * Returns the poster URI.
+ */
+ public String getPosterUri() {
+ return mPosterUri;
+ }
+
+ /**
+ * Returns the photo URI.
+ */
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ /**
+ * Returns the state of series recording.
+ */
+ @SeriesState public int getState() {
+ return mState;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SeriesRecording)) return false;
+ SeriesRecording that = (SeriesRecording) o;
+ return mPriority == that.mPriority
+ && mChannelId == that.mChannelId
+ && mStartFromSeason == that.mStartFromSeason
+ && mStartFromEpisode == that.mStartFromEpisode
+ && Objects.equals(mId, that.mId)
+ && Objects.equals(mTitle, that.mTitle)
+ && Objects.equals(mDescription, that.mDescription)
+ && Objects.equals(mLongDescription, that.mLongDescription)
+ && Objects.equals(mSeriesId, that.mSeriesId)
+ && mChannelOption == that.mChannelOption
+ && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds)
+ && Objects.equals(mPosterUri, that.mPosterUri)
+ && Objects.equals(mPhotoUri, that.mPhotoUri)
+ && mState == that.mState;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId,
+ mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption,
+ mCanonicalGenreIds, mPosterUri, mPhotoUri, mState);
+ }
+
+ @Override
+ public String toString() {
+ return "SeriesRecording{" +
+ "inputId=" + mInputId +
+ ", channelId=" + mChannelId +
+ ", id='" + mId + '\'' +
+ ", priority=" + mPriority +
+ ", title='" + mTitle + '\'' +
+ ", description='" + mDescription + '\'' +
+ ", longDescription='" + mLongDescription + '\'' +
+ ", startFromSeason=" + mStartFromSeason +
+ ", startFromEpisode=" + mStartFromEpisode +
+ ", channelOption=" + mChannelOption +
+ ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) +
+ ", posterUri=" + mPosterUri +
+ ", photoUri=" + mPhotoUri +
+ ", state=" + mState +
+ '}';
+ }
+
+ private SeriesRecording(long id, long priority, String title, String description,
+ String longDescription, String inputId, long channelId, String seriesId,
+ int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds,
+ String posterUri, String photoUri, int state) {
+ this.mId = id;
+ this.mPriority = priority;
+ this.mTitle = title;
+ this.mDescription = description;
+ this.mLongDescription = longDescription;
+ this.mInputId = inputId;
+ this.mChannelId = channelId;
+ this.mSeriesId = seriesId;
+ this.mStartFromSeason = startFromSeason;
+ this.mStartFromEpisode = startFromEpisode;
+ this.mChannelOption = channelOption;
+ this.mCanonicalGenreIds = canonicalGenreIds;
+ this.mPosterUri = posterUri;
+ this.mPhotoUri = photoUri;
+ this.mState = state;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int paramInt) {
+ out.writeLong(mId);
+ out.writeLong(mPriority);
+ out.writeString(mTitle);
+ out.writeString(mDescription);
+ out.writeString(mLongDescription);
+ out.writeString(mInputId);
+ out.writeLong(mChannelId);
+ out.writeString(mSeriesId);
+ out.writeInt(mStartFromSeason);
+ out.writeInt(mStartFromEpisode);
+ out.writeInt(mChannelOption);
+ out.writeIntArray(mCanonicalGenreIds);
+ out.writeString(mPosterUri);
+ out.writeString(mPhotoUri);
+ out.writeInt(mState);
+ }
+
+ /**
+ * Returns an array containing all of the elements in the list.
+ */
+ public static SeriesRecording[] toArray(Collection<SeriesRecording> series) {
+ return series.toArray(new SeriesRecording[series.size()]);
+ }
+
+ /**
+ * Returns {@code true} if the {@code program} is part of the series and meets the season and
+ * episode constraints.
+ */
+ public boolean matchProgram(Program program) {
+ return matchProgram(program, true);
+ }
+
+ /**
+ * Returns {@code true} if the {@code program} is part of the series and meets the season and
+ * episode constraints. It checks the channel option only if {@code checkChannelOption} is
+ * {@code true}.
+ */
+ public boolean matchProgram(Program program, boolean checkChannelOption) {
+ 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
+ && mChannelId != channelId)) {
+ return false;
+ }
+ // Season number and episode number matches if
+ // start_season_number < program_season_number
+ // || (start_season_number == program_season_number
+ // && start_episode_number <= program_episode_number).
+ if (mStartFromSeason == SeriesRecordings.THE_BEGINNING
+ || TextUtils.isEmpty(seasonNumber)) {
+ return true;
+ } else {
+ int intSeasonNumber;
+ try {
+ intSeasonNumber = Integer.valueOf(seasonNumber);
+ } catch (NumberFormatException e) {
+ return true;
+ }
+ if (intSeasonNumber > mStartFromSeason) {
+ return true;
+ } else if (intSeasonNumber < mStartFromSeason) {
+ return false;
+ }
+ }
+ if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING
+ || TextUtils.isEmpty(episodeNumber)) {
+ return true;
+ } else {
+ int intEpisodeNumber;
+ try {
+ intEpisodeNumber = Integer.valueOf(episodeNumber);
+ } catch (NumberFormatException e) {
+ return true;
+ }
+ return intEpisodeNumber >= mStartFromEpisode;
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java
new file mode 100644
index 00000000..9e9b3add
--- /dev/null
+++ b/src/com/android/tv/dvr/SeriesRecordingScheduler.java
@@ -0,0 +1,705 @@
+/*
+ * 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.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 com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.common.CollectionUtils;
+import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Program;
+import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
+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;
+import java.util.Collection;
+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.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}.
+ * <p>
+ * The current implementation assumes that the series recordings are scheduled only for one channel.
+ */
+@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 String KEY_FETCHED_SERIES_IDS =
+ "SeriesRecordingScheduler.fetched_series_ids";
+
+ @SuppressLint("StaticFieldLeak")
+ private static SeriesRecordingScheduler sInstance;
+
+ /**
+ * Creates and returns the {@link SeriesRecordingScheduler}.
+ */
+ public static synchronized SeriesRecordingScheduler getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new SeriesRecordingScheduler(context);
+ }
+ return sInstance;
+ }
+
+ private final Context mContext;
+ private final DvrManager mDvrManager;
+ private final WritableDvrDataManager mDataManager;
+ private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>();
+ private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>();
+ private final Set<String> mFetchedSeriesIds = new ArraySet<>();
+ private final SharedPreferences mSharedPreferences;
+ private boolean mStarted;
+
+ private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() {
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ executeFetchSeriesInfoTask(seriesRecording);
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ // Cancel the update.
+ for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
+ iter.hasNext(); ) {
+ SeriesRecordingUpdateTask task = iter.next();
+ if (CollectionUtils.subtract(task.mSeriesRecordings, seriesRecordings,
+ SeriesRecording.ID_COMPARATOR).isEmpty()) {
+ task.cancel(true);
+ iter.remove();
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ updateSchedules(Arrays.asList(seriesRecordings));
+ }
+ };
+
+ private final ScheduledRecordingListener mScheduledRecordingListener =
+ new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
+ // No need to update series recordings when the new schedule is added.
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ handleScheduledRecordingChange(Arrays.asList(schedules));
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ List<ScheduledRecording> schedulesForUpdate = new ArrayList<>();
+ for (ScheduledRecording r : schedules) {
+ if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
+ || r.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED)
+ && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
+ && !TextUtils.isEmpty(r.getSeasonNumber())
+ && !TextUtils.isEmpty(r.getEpisodeNumber())) {
+ schedulesForUpdate.add(r);
+ }
+ }
+ if (!schedulesForUpdate.isEmpty()) {
+ handleScheduledRecordingChange(schedulesForUpdate);
+ }
+ }
+
+ private void handleScheduledRecordingChange(List<ScheduledRecording> schedules) {
+ if (schedules.isEmpty()) {
+ return;
+ }
+ 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());
+ }
+ }
+ if (!seriesRecordingIds.isEmpty()) {
+ List<SeriesRecording> seriesRecordings = new ArrayList<>();
+ for (Long id : seriesRecordingIds) {
+ SeriesRecording seriesRecording = mDataManager.getSeriesRecording(id);
+ if (seriesRecording != null) {
+ seriesRecordings.add(seriesRecording);
+ }
+ }
+ if (!seriesRecordings.isEmpty()) {
+ updateSchedules(seriesRecordings);
+ }
+ }
+ }
+ };
+
+ private SeriesRecordingScheduler(Context context) {
+ mContext = context.getApplicationContext();
+ ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
+ mDvrManager = appSingletons.getDvrManager();
+ mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
+ mSharedPreferences = context.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE);
+ mFetchedSeriesIds.addAll(mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS,
+ Collections.emptySet()));
+ }
+
+ /**
+ * Starts the scheduler.
+ */
+ @MainThread
+ public void start() {
+ SoftPreconditions.checkState(mDataManager.isInitialized());
+ if (mStarted) {
+ return;
+ }
+ mStarted = true;
+ mDataManager.addSeriesRecordingListener(mSeriesRecordingListener);
+ mDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
+ startFetchingSeriesInfo();
+ updateSchedules(mDataManager.getSeriesRecordings());
+ }
+
+ @MainThread
+ public void stop() {
+ if (!mStarted) {
+ return;
+ }
+ mStarted = false;
+ for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) {
+ task.cancel(true);
+ }
+ for (SeriesRecordingUpdateTask task : mScheduleTasks) {
+ task.cancel(true);
+ }
+ mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
+ mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener);
+ }
+
+ private void startFetchingSeriesInfo() {
+ for (SeriesRecording seriesRecording : mDataManager.getSeriesRecordings()) {
+ if (!mFetchedSeriesIds.contains(seriesRecording.getSeriesId())) {
+ executeFetchSeriesInfoTask(seriesRecording);
+ }
+ }
+ }
+
+ private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) {
+ if (Experiments.CLOUD_EPG.get()) {
+ FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording);
+ task.execute();
+ mFetchSeriesInfoTasks.add(task);
+ }
+ }
+
+ /**
+ * Creates/Updates the schedules for all the series recordings.
+ */
+ @MainThread
+ public void updateSchedules() {
+ if (!mStarted) {
+ return;
+ }
+ updateSchedules(mDataManager.getSeriesRecordings());
+ }
+
+ private void updateSchedules(Collection<SeriesRecording> seriesRecordings) {
+ Set<SeriesRecording> previousSeriesRecordings = new HashSet<>();
+ for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
+ iter.hasNext(); ) {
+ SeriesRecordingUpdateTask task = iter.next();
+ if (CollectionUtils.containsAny(task.mSeriesRecordings, seriesRecordings,
+ SeriesRecording.ID_COMPARATOR)) {
+ // The task is affected by the seriesRecordings
+ task.cancel(true);
+ previousSeriesRecordings.addAll(task.mSeriesRecordings);
+ iter.remove();
+ }
+ }
+ List<SeriesRecording> seriesRecordingsToUpdate = CollectionUtils.union(seriesRecordings,
+ previousSeriesRecordings, SeriesRecording.ID_COMPARATOR);
+ for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator();
+ iter.hasNext(); ) {
+ if (mDataManager.getSeriesRecording(iter.next().getId()) == null) {
+ // Series recording has been removed.
+ 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);
+ mScheduleTasks.add(task);
+ } else {
+ for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
+ SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask(
+ Collections.singletonList(seriesRecording),
+ createSqlParams(Collections.singletonList(seriesRecording), null));
+ tasksToRun.add(task);
+ mScheduleTasks.add(task);
+ }
+ }
+ if (mDataManager.isDvrScheduleLoadFinished()) {
+ runTasks(tasksToRun);
+ }
+ }
+
+ private void runTasks(List<SeriesRecordingUpdateTask> tasks) {
+ for (SeriesRecordingUpdateTask task : tasks) {
+ task.executeOnDbThread();
+ }
+ }
+
+ private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) {
+ for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
+ if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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
+ * of them are added even though they are the same episodes. That's because the schedules
+ * should be added to the series recording.
+ * <p>If there are no existing schedules for an episode, one program which starts earlier is
+ * picked.
+ */
+ private Map<Long, List<Program>> pickOneProgramPerEpisode(
+ List<SeriesRecording> seriesRecordings, List<Program> programs) {
+ return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs);
+ }
+
+ /**
+ * @see #pickOneProgramPerEpisode(List, List)
+ */
+ @VisibleForTesting
+ static Map<Long, List<Program>> pickOneProgramPerEpisode(DvrDataManager dataManager,
+ List<SeriesRecording> seriesRecordings, List<Program> programs) {
+ // Initialize.
+ Map<Long, List<Program>> result = new HashMap<>();
+ Map<String, Long> seriesRecordingIds = new HashMap<>();
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ result.put(seriesRecording.getId(), new ArrayList<>());
+ seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId());
+ }
+ // Group programs by the episode.
+ Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>();
+ for (Program program : programs) {
+ long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId());
+ if (TextUtils.isEmpty(program.getSeasonNumber())
+ || TextUtils.isEmpty(program.getEpisodeNumber())) {
+ // Add all the programs if it doesn't have season number or episode number.
+ result.get(seriesRecordingId).add(program);
+ continue;
+ }
+ ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId,
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ List<Program> programsForEpisode = programsForEpisodeMap.get(episode);
+ if (programsForEpisode == null) {
+ programsForEpisode = new ArrayList<>();
+ programsForEpisodeMap.put(episode, programsForEpisode);
+ }
+ programsForEpisode.add(program);
+ }
+ // Pick one program.
+ for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) {
+ List<Program> programsForEpisode = entry.getValue();
+ Collections.sort(programsForEpisode, new Comparator<Program>() {
+ @Override
+ public int compare(Program lhs, Program rhs) {
+ // Place the existing schedule first.
+ boolean lhsScheduled = isProgramScheduled(dataManager, lhs);
+ boolean rhsScheduled = isProgramScheduled(dataManager, rhs);
+ if (lhsScheduled && !rhsScheduled) {
+ return -1;
+ }
+ if (!lhsScheduled && rhsScheduled) {
+ return 1;
+ }
+ // Sort by the start time in ascending order.
+ return lhs.compareTo(rhs);
+ }
+ });
+ boolean added = false;
+ // Add all the scheduled programs
+ List<Program> programsForSeries = result.get(entry.getKey().seriesRecordingId);
+ for (Program program : programsForEpisode) {
+ if (isProgramScheduled(dataManager, program)) {
+ programsForSeries.add(program);
+ added = true;
+ } else if (!added) {
+ programsForSeries.add(program);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) {
+ ScheduledRecording schedule =
+ dataManager.getScheduledRecordingForProgramId(program.getId());
+ return schedule != null && schedule.getState()
+ == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
+ }
+
+ private void updateFetchedSeries() {
+ mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply();
+ }
+
+ /**
+ * 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);
+ }
+
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ mScheduleTasks.remove(this);
+ if (programs == null) {
+ Log.e(TAG, "Creating schedules for series recording failed: " + mSeriesRecordings);
+ return;
+ }
+ Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode(
+ mSeriesRecordings, programs);
+ for (SeriesRecording seriesRecording : mSeriesRecordings) {
+ // Check the series recording is still valid.
+ if (mDataManager.getSeriesRecording(seriesRecording.getId()) == null) {
+ continue;
+ }
+ List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId());
+ if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null
+ && !programsToSchedule.isEmpty()) {
+ 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));
+ }
+ }
+ }
+
+ @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);
+ }
+
+ @Override
+ public String toString() {
+ return "ScheduledEpisode{" +
+ "seriesRecordingId=" + seriesRecordingId +
+ ", seasonNumber='" + seasonNumber +
+ ", episodeNumber=" + episodeNumber +
+ '}';
+ }
+ }
+
+ private class FetchSeriesInfoTask extends AsyncTask<Void, Void, SeriesInfo> {
+ private SeriesRecording mSeriesRecording;
+
+ FetchSeriesInfoTask(SeriesRecording seriesRecording) {
+ mSeriesRecording = seriesRecording;
+ }
+
+ String getSeriesId() {
+ return mSeriesRecording.getSeriesId();
+ }
+
+ @Override
+ protected SeriesInfo doInBackground(Void... voids) {
+ return EpgFetcher.createEpgReader(mContext)
+ .getSeriesInfo(mSeriesRecording.getSeriesId());
+ }
+
+ @Override
+ protected void onPostExecute(SeriesInfo seriesInfo) {
+ if (seriesInfo != null) {
+ mDataManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording)
+ .setTitle(seriesInfo.getTitle())
+ .setDescription(seriesInfo.getDescription())
+ .setLongDescription(seriesInfo.getLongDescription())
+ .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds())
+ .setPosterUri(seriesInfo.getPosterUri())
+ .setPhotoUri(seriesInfo.getPhotoUri())
+ .build());
+ mFetchedSeriesIds.add(seriesInfo.getId());
+ updateFetchedSeries();
+ }
+ mFetchSeriesInfoTasks.remove(this);
+ }
+
+ @Override
+ protected void onCancelled(SeriesInfo seriesInfo) {
+ mFetchSeriesInfoTasks.remove(this);
+ }
+ }
+
+ /**
+ * Called when the program loading is finished for the series recording.
+ */
+ public interface ProgramLoadCallback {
+ void onProgramLoadFinished(@NonNull List<Program> programs);
+ }
+}
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index 0b8a4c99..382f7112 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -18,6 +18,8 @@ package com.android.tv.dvr;
import android.support.annotation.MainThread;
+import com.android.tv.dvr.ScheduledRecording.RecordingState;
+
/**
* Full data manager.
*
@@ -27,27 +29,39 @@ import android.support.annotation.MainThread;
@MainThread
interface WritableDvrDataManager extends DvrDataManager {
/**
- * Add a new recording.
+ * Adds new recordings.
+ */
+ void addScheduledRecording(ScheduledRecording... scheduledRecordings);
+
+ /**
+ * Adds new series recordings.
+ */
+ void addSeriesRecording(SeriesRecording... seriesRecordings);
+
+ /**
+ * Removes recordings.
*/
- void addScheduledRecording(ScheduledRecording scheduledRecording);
+ void removeScheduledRecording(ScheduledRecording... scheduledRecordings);
/**
- * Add a season recording/
+ * Removes series recordings.
+ *
+ * <p>Note that the finished or failed schedules are not deleted.
*/
- void addSeasonRecording(SeasonRecording seasonRecording);
+ void removeSeriesRecording(SeriesRecording... seasonSchedules);
/**
- * Remove a recording.
+ * Updates existing recordings.
*/
- void removeScheduledRecording(ScheduledRecording ScheduledRecording);
+ void updateScheduledRecording(ScheduledRecording... scheduledRecordings);
/**
- * Remove a season schedule.
+ * Updates existing series recordings.
*/
- void removeSeasonSchedule(SeasonRecording seasonSchedule);
+ void updateSeriesRecording(SeriesRecording... seriesRecordings);
/**
- * Update an existing recording.
+ * Changes the state of the recording.
*/
- void updateScheduledRecording(ScheduledRecording r);
+ void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState);
}
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index 6058aa54..1a12fb23 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -22,7 +22,9 @@ import android.os.AsyncTask;
import android.support.annotation.Nullable;
import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.provider.DvrContract.Recordings;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.provider.DvrContract.Schedules;
+import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.NamedThreadFactory;
import java.util.ArrayList;
@@ -76,61 +78,59 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
protected abstract Result doInDvrBackground(Params... params);
/**
- * Inserts recordings returning the list of recordings with id set.
- * The id will be -1 if there was an error.
+ * Inserts schedules.
*/
- public abstract static class AsyncAddRecordingTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, List<ScheduledRecording>> {
-
- public AsyncAddRecordingTask(Context context) {
+ public static class AsyncAddScheduleTask
+ extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
+ public AsyncAddScheduleTask(Context context) {
super(context);
}
@Override
- protected final List<ScheduledRecording> doInDvrBackground(ScheduledRecording... params) {
- return sDbHelper.insertRecordings(params);
+ protected final Void doInDvrBackground(ScheduledRecording... params) {
+ sDbHelper.insertSchedules(params);
+ return null;
}
}
/**
- * Update recordings.
- *
- * @return list of row update counts. The count will be -1 if there was an error or 0
- * if no match was found. The count is expected to be exactly 1 for each recording.
+ * Update schedules.
*/
- public abstract static class AsyncUpdateRecordingTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> {
- public AsyncUpdateRecordingTask(Context context) {
+ public static class AsyncUpdateScheduleTask
+ extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
+ public AsyncUpdateScheduleTask(Context context) {
super(context);
}
@Override
- protected final List<Integer> doInDvrBackground(ScheduledRecording... params) {
- return sDbHelper.updateRecordings(params);
+ protected final Void doInDvrBackground(ScheduledRecording... params) {
+ sDbHelper.updateSchedules(params);
+ return null;
}
}
/**
- * Delete recordings.
- *
- * @return list of row delete counts. The count will be -1 if there was an error or 0
- * if no match was found. The count is expected to be exactly 1 for each recording.
+ * Delete schedules.
*/
- public abstract static class AsyncDeleteRecordingTask
- extends AsyncDvrDbTask<ScheduledRecording, Void, List<Integer>> {
- public AsyncDeleteRecordingTask(Context context) {
+ public static class AsyncDeleteScheduleTask
+ extends AsyncDvrDbTask<ScheduledRecording, Void, Void> {
+ public AsyncDeleteScheduleTask(Context context) {
super(context);
}
@Override
- protected final List<Integer> doInDvrBackground(ScheduledRecording... params) {
- return sDbHelper.deleteRecordings(params);
+ protected final Void doInDvrBackground(ScheduledRecording... params) {
+ sDbHelper.deleteSchedules(params);
+ return null;
}
}
- public abstract static class AsyncDvrQueryTask
+ /**
+ * Returns all {@link ScheduledRecording}s.
+ */
+ public abstract static class AsyncDvrQueryScheduleTask
extends AsyncDvrDbTask<Void, Void, List<ScheduledRecording>> {
- public AsyncDvrQueryTask(Context context) {
+ public AsyncDvrQueryScheduleTask(Context context) {
super(context);
}
@@ -140,17 +140,84 @@ public abstract class AsyncDvrDbTask<Params, Progress, Result>
if (isCancelled()) {
return null;
}
-
- if (isCancelled()) {
- return null;
+ List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
+ try (Cursor c = sDbHelper.query(Schedules.TABLE_NAME, ScheduledRecording.PROJECTION)) {
+ while (c.moveToNext() && !isCancelled()) {
+ scheduledRecordings.add(ScheduledRecording.fromCursor(c));
+ }
}
+ return scheduledRecordings;
+ }
+ }
+
+ /**
+ * Inserts series recordings.
+ */
+ public static class AsyncAddSeriesRecordingTask
+ extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
+ public AsyncAddSeriesRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final Void doInDvrBackground(SeriesRecording... params) {
+ sDbHelper.insertSeriesRecordings(params);
+ return null;
+ }
+ }
+
+ /**
+ * Update series recordings.
+ */
+ public static class AsyncUpdateSeriesRecordingTask
+ extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
+ public AsyncUpdateSeriesRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final Void doInDvrBackground(SeriesRecording... params) {
+ sDbHelper.updateSeriesRecordings(params);
+ return null;
+ }
+ }
+
+ /**
+ * Delete series recordings.
+ */
+ public static class AsyncDeleteSeriesRecordingTask
+ extends AsyncDvrDbTask<SeriesRecording, Void, Void> {
+ public AsyncDeleteSeriesRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected final Void doInDvrBackground(SeriesRecording... params) {
+ sDbHelper.deleteSeriesRecordings(params);
+ return null;
+ }
+ }
+
+ /**
+ * Returns all {@link SeriesRecording}s.
+ */
+ public abstract static class AsyncDvrQuerySeriesRecordingTask
+ extends AsyncDvrDbTask<Void, Void, List<SeriesRecording>> {
+ public AsyncDvrQuerySeriesRecordingTask(Context context) {
+ super(context);
+ }
+
+ @Override
+ @Nullable
+ protected final List<SeriesRecording> doInDvrBackground(Void... params) {
if (isCancelled()) {
return null;
}
- List<ScheduledRecording> scheduledRecordings = new ArrayList<>();
- try (Cursor c = sDbHelper.query(Recordings.TABLE_NAME, ScheduledRecording.PROJECTION)) {
+ List<SeriesRecording> scheduledRecordings = new ArrayList<>();
+ try (Cursor c = sDbHelper.query(SeriesRecordings.TABLE_NAME,
+ SeriesRecording.PROJECTION)) {
while (c.moveToNext() && !isCancelled()) {
- scheduledRecordings.add(ScheduledRecording.fromCursor(c));
+ scheduledRecordings.add(SeriesRecording.fromCursor(c));
}
}
return scheduledRecordings;
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index 192cc17b..3fe2d211 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -23,10 +23,10 @@ import android.provider.BaseColumns;
* columns. It's for the internal use in Live TV.
*/
public final class DvrContract {
- /** Column definition for Recording table. */
- public static final class Recordings implements BaseColumns {
+ /** Column definition for Schedules table. */
+ public static final class Schedules implements BaseColumns {
/** The table name. */
- public static final String TABLE_NAME = "recording";
+ public static final String TABLE_NAME = "schedules";
/** The recording type for program recording. */
public static final String TYPE_PROGRAM = "TYPE_PROGRAM";
@@ -34,22 +34,27 @@ public final class DvrContract {
/** The recording type for timed recording. */
public static final String TYPE_TIMED = "TYPE_TIMED";
- /** The recording type for season recording. */
- public static final String TYPE_SEASON_RECORDING = "TYPE_SEASON_RECORDING";
-
/** The recording has not been started yet. */
public static final String STATE_RECORDING_NOT_STARTED = "STATE_RECORDING_NOT_STARTED";
/** The recording is in progress. */
public static final String STATE_RECORDING_IN_PROGRESS = "STATE_RECORDING_IN_PROGRESS";
- /** The recording was unexpectedly stopped. */
- public static final String STATE_RECORDING_UNEXPECTEDLY_STOPPED =
- "STATE_RECORDING_UNEXPECTEDLY_STOPPED";
-
/** The recording is finished. */
public static final String STATE_RECORDING_FINISHED = "STATE_RECORDING_FINISHED";
+ /** The recording failed. */
+ public static final String STATE_RECORDING_FAILED = "STATE_RECORDING_FAILED";
+
+ /** The recording finished and clipping. */
+ public static final String STATE_RECORDING_CLIPPED = "STATE_RECORDING_CLIPPED";
+
+ /** The recording marked as deleted. */
+ public static final String STATE_RECORDING_DELETED = "STATE_RECORDING_DELETED";
+
+ /** The recording marked as canceled. */
+ public static final String STATE_RECORDING_CANCELED = "STATE_RECORDING_CANCELED";
+
/**
* The priority of this recording.
*
@@ -63,16 +68,25 @@ public final class DvrContract {
/**
* The type of this recording.
*
- * <p>This value should be one of the followings: {@link #TYPE_PROGRAM},
- * {@link #TYPE_TIMED}, and {@link #TYPE_SEASON_RECORDING}.
+ * <p>This value should be one of the followings: {@link #TYPE_PROGRAM} and
+ * {@link #TYPE_TIMED}.
*
* <p>This is a required field.
*
- * <p>Type: String
+ * <p>Type: TEXT
*/
public static final String COLUMN_TYPE = "type";
/**
+ * The input id of recording.
+ *
+ * <p>This is a required field.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_INPUT_ID = "input_id";
+
+ /**
* The ID of the channel for recording.
*
* <p>This is a required field.
@@ -81,9 +95,8 @@ public final class DvrContract {
*/
public static final String COLUMN_CHANNEL_ID = "channel_id";
-
/**
- * The ID of the associated program for recording.
+ * The ID of the associated program for recording.
*
* <p>This is an optional field.
*
@@ -92,6 +105,15 @@ public final class DvrContract {
public static final String COLUMN_PROGRAM_ID = "program_id";
/**
+ * The title of the associated program for recording.
+ *
+ * <p>This is an optional field.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PROGRAM_TITLE = "program_title";
+
+ /**
* The start time of this recording, in milliseconds since the epoch.
*
* <p>This is a required field.
@@ -110,19 +132,261 @@ public final class DvrContract {
public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
/**
+ * The season number of this program for episodic TV shows.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_SEASON_NUMBER = "season_number";
+
+ /**
+ * The episode number of this program for episodic TV shows.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_EPISODE_NUMBER = "episode_number";
+
+ /**
+ * The episode title of this program for episodic TV shows.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_EPISODE_TITLE = "episode_title";
+
+ /**
+ * The description of program.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PROGRAM_DESCRIPTION = "program_description";
+
+ /**
+ * The long description of program.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PROGRAM_LONG_DESCRIPTION = "program_long_description";
+
+ /**
+ * The poster art uri of program.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PROGRAM_POST_ART_URI = "program_poster_art_uri";
+
+ /**
+ * The thumbnail uri of program.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PROGRAM_THUMBNAIL_URI = "program_thumbnail_uri";
+
+ /**
* The state of this recording.
*
* <p>This value should be one of the followings: {@link #STATE_RECORDING_NOT_STARTED},
- * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED},
- * and {@link #STATE_RECORDING_FINISHED}.
+ * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED},
+ * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and
+ * {@link #STATE_RECORDING_DELETED}.
*
* <p>This is a required field.
*
- * <p>Type: String
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_STATE = "state";
+
+ /**
+ * The ID of the parent series recording.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_SERIES_RECORDING_ID = "series_recording_id";
+
+ private Schedules() { }
+ }
+
+ /** Column definition for Recording table. */
+ public static final class SeriesRecordings implements BaseColumns {
+ /** The table name. */
+ public static final String TABLE_NAME = "series_recording";
+
+ /**
+ * This value is used for {@link #COLUMN_START_FROM_SEASON} and
+ * {@link #COLUMN_START_FROM_EPISODE} to mean record all seasons or episodes.
+ */
+ public static final int THE_BEGINNING = -1;
+
+ /**
+ * The series recording option which indicates that the episodes in one channel are
+ * recorded.
+ */
+ public static final String OPTION_CHANNEL_ONE = "OPTION_CHANNEL_ONE";
+
+ /**
+ * The series recording option which indicates that the episodes in all the channels are
+ * recorded.
+ */
+ public static final String OPTION_CHANNEL_ALL = "OPTION_CHANNEL_ALL";
+
+ /**
+ * The state indicates that it is a normal one.
+ */
+ public static final String STATE_SERIES_NORMAL = "STATE_SERIES_NORMAL";
+
+ /**
+ * The state indicates that it is a canceled one.
+ */
+ public static final String STATE_SERIES_CANCELED = "STATE_SERIES_CANCELED";
+
+ /**
+ * The priority of this recording.
+ *
+ * <p> The lowest number is recorded first. If there is a tie in priority then the lower id
+ * wins. Defaults to {@value Long#MAX_VALUE}
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_PRIORITY = "priority";
+
+ /**
+ * The input id of recording.
+ *
+ * <p>This is a required field.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_INPUT_ID = "input_id";
+
+ /**
+ * The ID of the channel for recording.
+ *
+ * <p>This is a required field.
+ *
+ * <p>Type: INTEGER (long)
+ */
+ public static final String COLUMN_CHANNEL_ID = "channel_id";
+
+ /**
+ * The ID of the associated series to record.
+ *
+ * <p>The id is an opaque but stable string.
+ *
+ * <p>This is an optional field.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_SERIES_ID = "series_id";
+
+ /**
+ * The title of the series.
+ *
+ * <p>This is a required field.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_TITLE = "title";
+
+ /**
+ * The short description of the series.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_SHORT_DESCRIPTION = "short_description";
+
+ /**
+ * The long description of the series.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_LONG_DESCRIPTION = "long_description";
+
+ /**
+ * The number of the earliest season to record. The
+ * value {@link #THE_BEGINNING} means record all seasons.
+ *
+ * <p>Default value is {@value #THE_BEGINNING} {@link #THE_BEGINNING}.
+ *
+ * <p>Type: INTEGER (int)
+ */
+ public static final String COLUMN_START_FROM_SEASON = "start_from_season";
+
+ /**
+ * The number of the earliest episode to record in {@link #COLUMN_START_FROM_SEASON}. The
+ * value {@link #THE_BEGINNING} means record all episodes.
+ *
+ * <p>Default value is {@value #THE_BEGINNING} {@link #THE_BEGINNING}.
+ *
+ * <p>Type: INTEGER (int)
+ */
+ public static final String COLUMN_START_FROM_EPISODE = "start_from_episode";
+
+ /**
+ * The series recording option which indicates the channels to record.
+ *
+ * <p>This value should be one of the followings: {@link #OPTION_CHANNEL_ONE} and
+ * {@link #OPTION_CHANNEL_ALL}. The default value is OPTION_CHANNEL_ONE.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_CHANNEL_OPTION = "channel_option";
+
+ /**
+ * The comma-separated canonical genre string of this series.
+ *
+ * <p>Canonical genres are defined in {@link android.media.tv.TvContract.Programs.Genres}.
+ * Use {@link android.media.tv.TvContract.Programs.Genres#encode} to create a text that can
+ * be stored in this column. Use {@link android.media.tv.TvContract.Programs.Genres#decode}
+ * to get the canonical genre strings from the text stored in the column.
+ *
+ * <p>Type: TEXT
+ * @see android.media.tv.TvContract.Programs.Genres
+ * @see android.media.tv.TvContract.Programs.Genres#encode
+ * @see android.media.tv.TvContract.Programs.Genres#decode
+ */
+ public static final String COLUMN_CANONICAL_GENRE = "canonical_genre";
+
+ /**
+ * The URI for the poster of this TV series.
+ *
+ * <p>The data in the column must be a URL, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+ * </li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_POSTER_URI = "poster_uri";
+
+ /**
+ * The URI for the photo of this TV program.
+ *
+ * <p>The data in the column must be a URL, or a URI in one of the following formats:
+ *
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+ * </li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_PHOTO_URI = "photo_uri";
+
+ /**
+ * 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.
+ *
+ * <p>Type: TEXT
*/
public static final String COLUMN_STATE = "state";
- private Recordings() { }
+ private SeriesRecordings() { }
}
private DvrContract() { }
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index bdba8ac3..2f16ba5d 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -22,13 +22,15 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
import android.util.Log;
import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.provider.DvrContract.Recordings;
-
-import java.util.ArrayList;
-import java.util.List;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.provider.DvrContract.Schedules;
+import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
/**
* A data class for one recorded contents.
@@ -37,24 +39,153 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "DvrDatabaseHelper";
private static final boolean DEBUG = true;
- private static final int DATABASE_VERSION = 4;
+ private static final int DATABASE_VERSION = 17;
private static final String DB_NAME = "dvr.db";
- private static final String SQL_CREATE_RECORDINGS =
- "CREATE TABLE " + Recordings.TABLE_NAME + "("
- + Recordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
- + Recordings.COLUMN_PRIORITY + " INTEGER DEFAULT " + Long.MAX_VALUE + ","
- + Recordings.COLUMN_TYPE + " TEXT NOT NULL,"
- + Recordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL,"
- + Recordings.COLUMN_PROGRAM_ID + " INTEGER ,"
- + Recordings.COLUMN_START_TIME_UTC_MILLIS + " INTEGER NOT NULL,"
- + Recordings.COLUMN_END_TIME_UTC_MILLIS + " INTEGER NOT NULL,"
- + Recordings.COLUMN_STATE + " TEXT NOT NULL)";
-
- private static final String SQL_DROP_RECORDINGS = "DROP TABLE IF EXISTS "
- + Recordings.TABLE_NAME;
- public static final String WHERE_RECORDING_ID_EQUALS = Recordings._ID + " = ?";
+ private static final String SQL_CREATE_SCHEDULES =
+ "CREATE TABLE " + Schedules.TABLE_NAME + "("
+ + Schedules._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + Schedules.COLUMN_PRIORITY + " INTEGER DEFAULT "
+ + ScheduledRecording.DEFAULT_PRIORITY + ","
+ + Schedules.COLUMN_TYPE + " TEXT NOT NULL,"
+ + Schedules.COLUMN_INPUT_ID + " TEXT NOT NULL,"
+ + Schedules.COLUMN_CHANNEL_ID + " INTEGER NOT NULL,"
+ + Schedules.COLUMN_PROGRAM_ID + " INTEGER,"
+ + Schedules.COLUMN_PROGRAM_TITLE + " TEXT,"
+ + Schedules.COLUMN_START_TIME_UTC_MILLIS + " INTEGER NOT NULL,"
+ + Schedules.COLUMN_END_TIME_UTC_MILLIS + " INTEGER NOT NULL,"
+ + Schedules.COLUMN_SEASON_NUMBER + " TEXT,"
+ + Schedules.COLUMN_EPISODE_NUMBER + " TEXT,"
+ + Schedules.COLUMN_EPISODE_TITLE + " TEXT,"
+ + Schedules.COLUMN_PROGRAM_DESCRIPTION + " TEXT,"
+ + Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION + " TEXT,"
+ + Schedules.COLUMN_PROGRAM_POST_ART_URI + " TEXT,"
+ + Schedules.COLUMN_PROGRAM_THUMBNAIL_URI + " TEXT,"
+ + Schedules.COLUMN_STATE + " TEXT NOT NULL,"
+ + Schedules.COLUMN_SERIES_RECORDING_ID + " INTEGER,"
+ + "FOREIGN KEY(" + Schedules.COLUMN_SERIES_RECORDING_ID + ") "
+ + "REFERENCES " + SeriesRecordings.TABLE_NAME
+ + "(" + SeriesRecordings._ID + ") "
+ + "ON UPDATE CASCADE ON DELETE SET NULL);";
+
+ private static final String SQL_DROP_SCHEDULES = "DROP TABLE IF EXISTS " + Schedules.TABLE_NAME;
+
+ private static final String SQL_CREATE_SERIES_RECORDINGS =
+ "CREATE TABLE " + SeriesRecordings.TABLE_NAME + "("
+ + SeriesRecordings._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + SeriesRecordings.COLUMN_PRIORITY + " INTEGER DEFAULT "
+ + SeriesRecording.DEFAULT_PRIORITY + ","
+ + SeriesRecordings.COLUMN_TITLE + " TEXT NOT NULL,"
+ + SeriesRecordings.COLUMN_SHORT_DESCRIPTION + " TEXT,"
+ + SeriesRecordings.COLUMN_LONG_DESCRIPTION + " TEXT,"
+ + SeriesRecordings.COLUMN_INPUT_ID + " TEXT NOT NULL,"
+ + SeriesRecordings.COLUMN_CHANNEL_ID + " INTEGER NOT NULL,"
+ + SeriesRecordings.COLUMN_SERIES_ID + " TEXT NOT NULL,"
+ + SeriesRecordings.COLUMN_START_FROM_SEASON + " INTEGER DEFAULT "
+ + SeriesRecordings.THE_BEGINNING + ","
+ + SeriesRecordings.COLUMN_START_FROM_EPISODE + " INTEGER DEFAULT "
+ + SeriesRecordings.THE_BEGINNING + ","
+ + SeriesRecordings.COLUMN_CHANNEL_OPTION + " TEXT DEFAULT "
+ + SeriesRecordings.OPTION_CHANNEL_ONE + ","
+ + SeriesRecordings.COLUMN_CANONICAL_GENRE + " TEXT,"
+ + SeriesRecordings.COLUMN_POSTER_URI + " TEXT,"
+ + SeriesRecordings.COLUMN_PHOTO_URI + " TEXT,"
+ + SeriesRecordings.COLUMN_STATE + " TEXT)";
+
+ private static final String SQL_DROP_SERIES_RECORDINGS = "DROP TABLE IF EXISTS " +
+ SeriesRecordings.TABLE_NAME;
+
+ private static final int SQL_DATA_TYPE_LONG = 0;
+ private static final int SQL_DATA_TYPE_INT = 1;
+ private static final int SQL_DATA_TYPE_STRING = 2;
+
+ private static final ColumnInfo[] COLUMNS_SCHEDULES = new ColumnInfo[] {
+ new ColumnInfo(Schedules._ID, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_TYPE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_ID, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_TITLE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_START_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_END_TIME_UTC_MILLIS, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(Schedules.COLUMN_SEASON_NUMBER, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_EPISODE_NUMBER, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_EPISODE_TITLE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_DESCRIPTION, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_POST_ART_URI, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_STATE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_SERIES_RECORDING_ID, SQL_DATA_TYPE_LONG)};
+ private static final String SQL_INSERT_SCHEDULES =
+ buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
+ private static final String SQL_UPDATE_SCHEDULES =
+ buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
+ private static final String SQL_DELETE_SCHEDULES = buildDeleteSql(Schedules.TABLE_NAME);
+
+ private static final ColumnInfo[] COLUMNS_SERIES_RECORDINGS = new ColumnInfo[] {
+ new ColumnInfo(SeriesRecordings._ID, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(SeriesRecordings.COLUMN_PRIORITY, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(SeriesRecordings.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG),
+ new ColumnInfo(SeriesRecordings.COLUMN_SERIES_ID, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_TITLE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_SEASON, SQL_DATA_TYPE_INT),
+ new ColumnInfo(SeriesRecordings.COLUMN_START_FROM_EPISODE, SQL_DATA_TYPE_INT),
+ new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_OPTION, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_CANONICAL_GENRE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_POSTER_URI, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_PHOTO_URI, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(SeriesRecordings.COLUMN_STATE, SQL_DATA_TYPE_STRING)};
+
+ private static final String SQL_INSERT_SERIES_RECORDINGS =
+ buildInsertSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS);
+ private static final String SQL_UPDATE_SERIES_RECORDINGS =
+ buildUpdateSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS);
+ private static final String SQL_DELETE_SERIES_RECORDINGS =
+ buildDeleteSql(SeriesRecordings.TABLE_NAME);
+
+ private static String buildInsertSql(String tableName, ColumnInfo[] columns) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("INSERT INTO ").append(tableName).append(" (");
+ boolean appendComma = false;
+ for (ColumnInfo columnInfo : columns) {
+ if (appendComma) {
+ sb.append(",");
+ }
+ appendComma = true;
+ sb.append(columnInfo.name);
+ }
+ sb.append(") VALUES (?");
+ for (int i = 1; i < columns.length; ++i) {
+ sb.append(",?");
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+
+ private static String buildUpdateSql(String tableName, ColumnInfo[] columns) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("UPDATE ").append(tableName).append(" SET ");
+ boolean appendComma = false;
+ for (ColumnInfo columnInfo : columns) {
+ if (appendComma) {
+ sb.append(",");
+ }
+ appendComma = true;
+ sb.append(columnInfo.name).append("=?");
+ }
+ sb.append(" WHERE ").append(BaseColumns._ID).append("=?");
+ return sb.toString();
+ }
+
+ private static String buildDeleteSql(String tableName) {
+ return "DELETE FROM " + tableName + " WHERE " + BaseColumns._ID + "=?";
+ }
public DvrDatabaseHelper(Context context) {
super(context.getApplicationContext(), DB_NAME, null, DATABASE_VERSION);
}
@@ -66,14 +197,18 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
- if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_RECORDINGS);
- db.execSQL(SQL_CREATE_RECORDINGS);
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES);
+ db.execSQL(SQL_CREATE_SCHEDULES);
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SERIES_RECORDINGS);
+ db.execSQL(SQL_CREATE_SERIES_RECORDINGS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_RECORDINGS);
- db.execSQL(SQL_DROP_RECORDINGS);
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES);
+ db.execSQL(SQL_DROP_SCHEDULES);
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
+ db.execSQL(SQL_DROP_SERIES_RECORDINGS);
onCreate(db);
}
@@ -88,61 +223,164 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
}
/**
- * Inserts recordings.
- *
- * @return The list of recordings with id set. The id will be -1 if there was an error.
+ * Inserts schedules.
*/
- public List<ScheduledRecording> insertRecordings(ScheduledRecording... scheduledRecordings) {
- updateChannelsFromRecordings(scheduledRecordings);
+ public void insertSchedules(ScheduledRecording... scheduledRecordings) {
+ SQLiteDatabase db = getWritableDatabase();
+ SQLiteStatement statement = db.compileStatement(SQL_INSERT_SCHEDULES);
+ db.beginTransaction();
+ try {
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SCHEDULES, values);
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
- SQLiteDatabase db = getReadableDatabase();
- List<ScheduledRecording> results = new ArrayList<>();
- for (ScheduledRecording r : scheduledRecordings) {
- ContentValues values = ScheduledRecording.toContentValues(r);
- long id = db.insert(Recordings.TABLE_NAME, null, values);
- results.add(ScheduledRecording.buildFrom(r).setId(id).build());
+ /**
+ * Update schedules.
+ */
+ public void updateSchedules(ScheduledRecording... scheduledRecordings) {
+ SQLiteDatabase db = getWritableDatabase();
+ SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SCHEDULES);
+ db.beginTransaction();
+ try {
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ ContentValues values = ScheduledRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SCHEDULES, values);
+ statement.bindLong(COLUMNS_SCHEDULES.length + 1, r.getId());
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Delete schedules.
+ */
+ public void deleteSchedules(ScheduledRecording... scheduledRecordings) {
+ SQLiteDatabase db = getWritableDatabase();
+ SQLiteStatement statement = db.compileStatement(SQL_DELETE_SCHEDULES);
+ db.beginTransaction();
+ try {
+ for (ScheduledRecording r : scheduledRecordings) {
+ statement.clearBindings();
+ statement.bindLong(1, r.getId());
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
}
- return results;
}
/**
- * Update recordings.
- *
- * @return The list of row update counts. The count will be -1 if there was an error or 0
- * if no match was found. The count is expected to be exactly 1 for each recording.
+ * Inserts series recordings.
*/
- public List<Integer> updateRecordings(ScheduledRecording[] scheduledRecordings) {
- updateChannelsFromRecordings(scheduledRecordings);
+ public void insertSeriesRecordings(SeriesRecording... seriesRecordings) {
SQLiteDatabase db = getWritableDatabase();
- List<Integer> results = new ArrayList<>();
- for (ScheduledRecording r : scheduledRecordings) {
- ContentValues values = ScheduledRecording.toContentValues(r);
- int updated = db.update(Recordings.TABLE_NAME, values, Recordings._ID + " = ?",
- new String[] {String.valueOf(r.getId())});
- results.add(updated);
+ SQLiteStatement statement = db.compileStatement(SQL_INSERT_SERIES_RECORDINGS);
+ db.beginTransaction();
+ try {
+ for (SeriesRecording r : seriesRecordings) {
+ statement.clearBindings();
+ ContentValues values = SeriesRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SERIES_RECORDINGS, values);
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
}
- return results;
}
- private void updateChannelsFromRecordings(ScheduledRecording[] scheduledRecordings) {
- // TODO(DVR) implement/
- // TODO(DVR) consider not deleting channels instead of keeping a separate table.
+ /**
+ * Update series recordings.
+ */
+ public void updateSeriesRecordings(SeriesRecording... seriesRecordings) {
+ SQLiteDatabase db = getWritableDatabase();
+ SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SERIES_RECORDINGS);
+ db.beginTransaction();
+ try {
+ for (SeriesRecording r : seriesRecordings) {
+ statement.clearBindings();
+ ContentValues values = SeriesRecording.toContentValues(r);
+ bindColumns(statement, COLUMNS_SERIES_RECORDINGS, values);
+ statement.bindLong(COLUMNS_SERIES_RECORDINGS.length + 1, r.getId());
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
}
/**
- * Delete recordings.
- *
- * @return The list of row update counts. The count will be -1 if there was an error or 0
- * if no match was found. The count is expected to be exactly 1 for each recording.
+ * Delete series recordings.
*/
- public List<Integer> deleteRecordings(ScheduledRecording[] scheduledRecordings) {
+ public void deleteSeriesRecordings(SeriesRecording... seriesRecordings) {
SQLiteDatabase db = getWritableDatabase();
- List<Integer> results = new ArrayList<>();
- for (ScheduledRecording r : scheduledRecordings) {
- int deleted = db.delete(Recordings.TABLE_NAME, WHERE_RECORDING_ID_EQUALS,
- new String[] {String.valueOf(r.getId())});
- results.add(deleted);
+ SQLiteStatement statement = db.compileStatement(SQL_DELETE_SERIES_RECORDINGS);
+ db.beginTransaction();
+ try {
+ for (SeriesRecording r : seriesRecordings) {
+ statement.clearBindings();
+ statement.bindLong(1, r.getId());
+ statement.execute();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ private void bindColumns(SQLiteStatement statement, ColumnInfo[] columns,
+ ContentValues values) {
+ for (int i = 0; i < columns.length; ++i) {
+ ColumnInfo columnInfo = columns[i];
+ Object value = values.get(columnInfo.name);
+ switch (columnInfo.type) {
+ case SQL_DATA_TYPE_LONG:
+ if (value == null) {
+ statement.bindNull(i + 1);
+ } else {
+ statement.bindLong(i + 1, (Long) value);
+ }
+ break;
+ case SQL_DATA_TYPE_INT:
+ if (value == null) {
+ statement.bindNull(i + 1);
+ } else {
+ statement.bindLong(i + 1, (Integer) value);
+ }
+ break;
+ case SQL_DATA_TYPE_STRING: {
+ if (TextUtils.isEmpty((String) value)) {
+ statement.bindNull(i + 1);
+ } else {
+ statement.bindString(i + 1, (String) value);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private static class ColumnInfo {
+ final String name;
+ final int type;
+
+ ColumnInfo(String name, int type) {
+ this.name = name;
+ this.type = type;
}
- return results;
}
}
diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java
new file mode 100644
index 00000000..8b8cd5c5
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java
@@ -0,0 +1,138 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+// This class is adapted from Leanback's library, which does not support action icon with one-line
+// label. This class modified its getPresenter method to support the above situation.
+class ActionPresenterSelector extends PresenterSelector {
+ private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
+ private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
+ private final Presenter[] mPresenters = new Presenter[] {
+ mOneLineActionPresenter, mTwoLineActionPresenter};
+
+ @Override
+ public Presenter getPresenter(Object item) {
+ Action action = (Action) item;
+ if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) {
+ return mOneLineActionPresenter;
+ } else {
+ return mTwoLineActionPresenter;
+ }
+ }
+
+ @Override
+ public Presenter[] getPresenters() {
+ return mPresenters;
+ }
+
+ static class ActionViewHolder extends Presenter.ViewHolder {
+ Action mAction;
+ Button mButton;
+ int mLayoutDirection;
+
+ public ActionViewHolder(View view, int layoutDirection) {
+ super(view);
+ mButton = (Button) view.findViewById(R.id.lb_action_button);
+ mLayoutDirection = layoutDirection;
+ }
+ }
+
+ class OneLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_1_line, parent, false);
+ return new ActionViewHolder(v, parent.getLayoutDirection());
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mAction = action;
+ vh.mButton.setText(action.getLabel1());
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ((ActionViewHolder) viewHolder).mAction = null;
+ }
+ }
+
+ class TwoLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_2_lines, parent, false);
+ return new ActionViewHolder(v, parent.getLayoutDirection());
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ Drawable icon = action.getIcon();
+ vh.mAction = action;
+
+ if (icon != null) {
+ final int startPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start);
+ final int endPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end);
+ vh.view.setPaddingRelative(startPadding, 0, endPadding, 0);
+ } else {
+ final int padding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal);
+ vh.view.setPaddingRelative(padding, 0, padding, 0);
+ }
+ if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) {
+ vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null);
+ } else {
+ vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
+ }
+
+ CharSequence line1 = action.getLabel1();
+ CharSequence line2 = action.getLabel2();
+ if (TextUtils.isEmpty(line1)) {
+ vh.mButton.setText(line2);
+ } else if (TextUtils.isEmpty(line2)) {
+ vh.mButton.setText(line1);
+ } else {
+ vh.mButton.setText(line1 + "\n" + line2);
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ vh.view.setPadding(0, 0, 0, 0);
+ vh.mAction = null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
new file mode 100644
index 00000000..5d8e20ff
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.res.Resources;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrManager;
+
+/**
+ * {@link RecordingDetailsFragment} for current recording in DVR.
+ */
+public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
+ private static final int ACTION_STOP_RECORDING = 1;
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING,
+ res.getString(R.string.epg_dvr_dialog_message_stop_recording), null,
+ res.getDrawable(R.drawable.lb_ic_stop)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_STOP_RECORDING) {
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity())
+ .getDvrManager();
+ dvrManager.stopRecording(getRecording());
+ }
+ getActivity().finish();
+ }
+ };
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DetailsContent.java b/src/com/android/tv/dvr/ui/DetailsContent.java
new file mode 100644
index 00000000..19521fca
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DetailsContent.java
@@ -0,0 +1,207 @@
+/*
+ * 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.media.tv.TvContract;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.Channel;
+
+/**
+ * A class for details content.
+ */
+public class DetailsContent {
+ /** Constant for invalid time. */
+ public static final long INVALID_TIME = -1;
+
+ private CharSequence mTitle;
+ private long mStartTimeUtcMillis;
+ private long mEndTimeUtcMillis;
+ private String mDescription;
+ private String mLogoImageUri;
+ private String mBackgroundImageUri;
+
+ private DetailsContent() { }
+
+ /**
+ * Returns title.
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns start time.
+ */
+ public long getStartTimeUtcMillis() {
+ return mStartTimeUtcMillis;
+ }
+
+ /**
+ * Returns end time.
+ */
+ public long getEndTimeUtcMillis() {
+ return mEndTimeUtcMillis;
+ }
+
+ /**
+ * Returns description.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns Logo image URI as a String.
+ */
+ public String getLogoImageUri() {
+ return mLogoImageUri;
+ }
+
+ /**
+ * Returns background image URI as a String.
+ */
+ public String getBackgroundImageUri() {
+ return mBackgroundImageUri;
+ }
+
+ /**
+ * Copies other details content.
+ */
+ public void copyFrom(DetailsContent other) {
+ if (this == other) {
+ return;
+ }
+ mTitle = other.mTitle;
+ mStartTimeUtcMillis = other.mStartTimeUtcMillis;
+ mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+ mDescription = other.mDescription;
+ mLogoImageUri = other.mLogoImageUri;
+ mBackgroundImageUri = other.mBackgroundImageUri;
+ }
+
+ /**
+ * A class for building details content.
+ */
+ public static final class Builder {
+ private final DetailsContent mDetailsContent;
+
+ public Builder() {
+ mDetailsContent = new DetailsContent();
+ mDetailsContent.mStartTimeUtcMillis = INVALID_TIME;
+ mDetailsContent.mEndTimeUtcMillis = INVALID_TIME;
+ }
+
+ /**
+ * Sets title.
+ */
+ public Builder setTitle(CharSequence title) {
+ mDetailsContent.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets start time.
+ */
+ public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
+ mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis;
+ return this;
+ }
+
+ /**
+ * Sets end time.
+ */
+ public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
+ mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis;
+ return this;
+ }
+
+ /**
+ * Sets description.
+ */
+ public Builder setDescription(String description) {
+ mDetailsContent.mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets logo image URI as a String.
+ */
+ public Builder setLogoImageUri(String logoImageUri) {
+ mDetailsContent.mLogoImageUri = logoImageUri;
+ return this;
+ }
+
+ /**
+ * Sets background image URI as a String.
+ */
+ public Builder setBackgroundImageUri(String backgroundImageUri) {
+ mDetailsContent.mBackgroundImageUri = backgroundImageUri;
+ return this;
+ }
+
+ /**
+ * Sets background image and logo image URI from program and channel.
+ */
+ public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) {
+ if (program != null) {
+ return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel);
+ } else {
+ return setImageUris(null, null, channel);
+ }
+ }
+
+ /**
+ * Sets background image and logo image URI and channel is used for fallback images.
+ */
+ public Builder setImageUris(@Nullable String posterArtUri,
+ @Nullable String thumbnailUri, @Nullable Channel channel) {
+ mDetailsContent.mLogoImageUri = null;
+ mDetailsContent.mBackgroundImageUri = null;
+ if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) {
+ mDetailsContent.mLogoImageUri = posterArtUri;
+ mDetailsContent.mBackgroundImageUri = thumbnailUri;
+ } else if (!TextUtils.isEmpty(posterArtUri)) {
+ // thumbnailUri is empty
+ mDetailsContent.mLogoImageUri = posterArtUri;
+ mDetailsContent.mBackgroundImageUri = posterArtUri;
+ } else if (!TextUtils.isEmpty(thumbnailUri)) {
+ // posterArtUri is empty
+ mDetailsContent.mLogoImageUri = thumbnailUri;
+ mDetailsContent.mBackgroundImageUri = thumbnailUri;
+ }
+ if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) {
+ String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId())
+ .toString();
+ mDetailsContent.mLogoImageUri = channelLogoUri;
+ mDetailsContent.mBackgroundImageUri = channelLogoUri;
+ }
+ return this;
+ }
+
+ /**
+ * Builds details content.
+ */
+ public DetailsContent build() {
+ DetailsContent detailsContent = new DetailsContent();
+ detailsContent.copyFrom(mDetailsContent);
+ return detailsContent;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
new file mode 100644
index 00000000..d6e17161
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
@@ -0,0 +1,42 @@
+/*
+ * 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.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+
+import com.android.tv.util.Utils;
+
+/**
+ * Presents a {@link DetailsContent}.
+ */
+public class DetailsContentPresenter extends AbstractDetailsDescriptionPresenter {
+ @Override
+ protected void onBindDescription(final ViewHolder viewHolder, Object itemData) {
+ DetailsContent detailsContent = (DetailsContent) itemData;
+ Context context = viewHolder.view.getContext();
+ viewHolder.getTitle().setText(detailsContent.getTitle());
+ if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME
+ && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) {
+ String playTime = Utils.getDurationString(context,
+ detailsContent.getStartTimeUtcMillis(),
+ detailsContent.getEndTimeUtcMillis(), false);
+ viewHolder.getSubtitle().setText(playTime);
+ }
+ viewHolder.getBody().setText(detailsContent.getDescription());
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
new file mode 100644
index 00000000..37f152f9
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
@@ -0,0 +1,88 @@
+/*
+ * 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.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.v17.leanback.app.BackgroundManager;
+
+/**
+ * The Background Helper.
+ */
+public class DetailsViewBackgroundHelper {
+ // Background delay serves to avoid kicking off expensive bitmap loading
+ // in case multiple backgrounds are set in quick succession.
+ private static final int SET_BACKGROUND_DELAY_MS = 100;
+
+ private final BackgroundManager mBackgroundManager;
+
+ class LoadBackgroundRunnable implements Runnable {
+ final Drawable mBackGround;
+
+ LoadBackgroundRunnable(Drawable background) {
+ mBackGround = background;
+ }
+
+ @Override
+ public void run() {
+ if (!mBackgroundManager.isAttached()) {
+ return;
+ }
+ if (mBackGround instanceof BitmapDrawable) {
+ mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap());
+ }
+ mRunnable = null;
+ }
+ }
+
+ private LoadBackgroundRunnable mRunnable;
+
+ private final Handler mHandler = new Handler();
+
+ public DetailsViewBackgroundHelper(Activity activity) {
+ mBackgroundManager = BackgroundManager.getInstance(activity);
+ mBackgroundManager.attach(activity.getWindow());
+ }
+
+ /**
+ * Sets the given image to background.
+ */
+ public void setBackground(Drawable background) {
+ if (mRunnable != null) {
+ mHandler.removeCallbacks(mRunnable);
+ }
+ mRunnable = new LoadBackgroundRunnable(background);
+ mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS);
+ }
+
+ /**
+ * Sets the background color.
+ */
+ public void setBackgroundColor(int color) {
+ mBackgroundManager.setColor(color);
+ }
+
+ /**
+ * Sets the background scrim.
+ */
+ public void setScrim(int color) {
+ mBackgroundManager.setDimLayer(new ColorDrawable(color));
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/DvrActivity.java
index 01f3fb9c..45fb1cf1 100644
--- a/src/com/android/tv/dvr/ui/DvrActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrActivity.java
@@ -20,6 +20,7 @@ import android.app.Activity;
import android.os.Bundle;
import com.android.tv.R;
+import com.android.tv.TvApplication;
/**
* {@link android.app.Activity} for DVR UI.
@@ -27,6 +28,7 @@ import com.android.tv.R;
public class DvrActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.dvr_main);
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
new file mode 100644
index 00000000..d7c2de88
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -0,0 +1,103 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.util.Utils;
+
+import java.util.List;
+
+/**
+ * A fragment which notifies the user that the same episode has already been scheduled.
+ *
+ * <p>Note that the schedule has not been created yet.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_RECORD_ANYWAY = 1;
+ private static final int ACTION_WATCH = 2;
+ private static final int ACTION_CANCEL = 3;
+
+ private Program mProgram;
+ private RecordedProgram mDuplicate;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
+ DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDuplicate = dvrManager.getRecordedProgram(mProgram.getTitle(),
+ mProgram.getSeasonNumber(), mProgram.getEpisodeNumber());
+ if (mDuplicate == null) {
+ dvrManager.addSchedule(mProgram);
+ DvrUiHelper.showAddScheduleToast(context, mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ }
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.dvr_already_recorded_dialog_title);
+ String description = getString(R.string.dvr_already_recorded_dialog_description);
+ Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ return new Guidance(title, description, null, image);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Context context = getContext();
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_RECORD_ANYWAY)
+ .title(R.string.dvr_action_record_anyway)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_WATCH)
+ .title(R.string.dvr_action_watch_now)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_CANCEL)
+ .title(R.string.dvr_action_record_cancel)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_RECORD_ANYWAY) {
+ getDvrManager().addSchedule(mProgram);
+ } else if (action.getId() == ACTION_WATCH) {
+ DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
new file mode 100644
index 00000000..78f21784
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -0,0 +1,107 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.text.format.DateUtils;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+import java.util.List;
+
+/**
+ * A fragment which notifies the user that the same episode has already been scheduled.
+ *
+ * <p>Note that the schedule has not been created yet.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_RECORD_ANYWAY = 1;
+ private static final int ACTION_RECORD_INSTEAD = 2;
+ private static final int ACTION_CANCEL = 3;
+
+ private Program mProgram;
+ private ScheduledRecording mDuplicate;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
+ DvrManager dvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDuplicate = dvrManager.getScheduledRecording(mProgram.getTitle(),
+ mProgram.getSeasonNumber(), mProgram.getEpisodeNumber());
+ if (mDuplicate == null) {
+ dvrManager.addSchedule(mProgram);
+ DvrUiHelper.showAddScheduleToast(context, mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ }
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.dvr_already_scheduled_dialog_title);
+ String description = getString(R.string.dvr_already_scheduled_dialog_description,
+ DateUtils.formatDateTime(getContext(), mDuplicate.getStartTimeMs(),
+ DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE));
+ Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ return new Guidance(title, description, null, image);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Context context = getContext();
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_RECORD_ANYWAY)
+ .title(R.string.dvr_action_record_anyway)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_RECORD_INSTEAD)
+ .title(R.string.dvr_action_record_instead)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_CANCEL)
+ .title(R.string.dvr_action_record_cancel)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_RECORD_ANYWAY) {
+ getDvrManager().addSchedule(mProgram);
+ } else if (action.getId() == ACTION_RECORD_INSTEAD) {
+ getDvrManager().addSchedule(mProgram);
+ getDvrManager().removeScheduledRecording(mDuplicate);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
index 70e71cab..74d0ba0b 100644
--- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
@@ -16,140 +16,591 @@
package com.android.tv.dvr.ui;
+import android.content.Context;
+import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Bundle;
-import android.support.annotation.IntDef;
+import android.os.Handler;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
-import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.text.TextUtils;
import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.recording.RecordedProgram;
+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.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.util.TvInputManagerHelper;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.LinkedHashMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
/**
* {@link BrowseFragment} for DVR functions.
*/
-public class DvrBrowseFragment extends BrowseFragment {
+public class DvrBrowseFragment extends BrowseFragment implements
+ RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener,
+ OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener {
private static final String TAG = "DvrBrowseFragment";
private static final boolean DEBUG = false;
- private ScheduledRecordingsAdapter mRecordingsInProgressAdapter;
- private ScheduledRecordingsAdapter mRecordingsNotStatedAdapter;
- private RecordedProgramsAdapter mRecordedProgramsAdapter;
-
- @IntDef({DVR_CURRENT_RECORDINGS, DVR_SCHEDULED_RECORDINGS, DVR_RECORDED_PROGRAMS, DVR_SETTINGS})
- @Retention(RetentionPolicy.SOURCE)
- public @interface DVR_HEADERS_MODE {}
- public static final int DVR_CURRENT_RECORDINGS = 0;
- public static final int DVR_SCHEDULED_RECORDINGS = 1;
- public static final int DVR_RECORDED_PROGRAMS = 2;
- public static final int DVR_SETTINGS = 3;
-
- private static final LinkedHashMap<Integer, Integer> sHeaders =
- new LinkedHashMap<Integer, Integer>() {{
- put(DVR_CURRENT_RECORDINGS, R.string.dvr_main_current_recordings);
- put(DVR_SCHEDULED_RECORDINGS, R.string.dvr_main_scheduled_recordings);
- put(DVR_RECORDED_PROGRAMS, R.string.dvr_main_recorded_programs);
- /* put(DVR_SETTINGS, R.string.dvr_main_settings); */ // TODO: Temporarily remove it for DP.
- }};
+ private static final int MAX_RECENT_ITEM_COUNT = 10;
+ private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
+ private RecordedProgramAdapter mRecentAdapter;
+ private ScheduleAdapter mScheduleAdapter;
+ private RecordedProgramAdapter mSeriesAdapter;
+ private RecordedProgramAdapter[] mGenreAdapters =
+ new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
+ private ListRow mRecentRow;
+ private ListRow mSeriesRow;
+ private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
+ private List<String> mGenreLabels;
private DvrDataManager mDvrDataManager;
+ private TvInputManagerHelper mTvInputManagerHelper;
private ArrayObjectAdapter mRowsAdapter;
+ private ClassPresenterSelector mPresenterSelector;
+ private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
+ private final Handler mHandler = new Handler();
+
+ private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ if (lhs instanceof SeriesRecording) {
+ lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
+ }
+ if (rhs instanceof SeriesRecording) {
+ rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
+ }
+ if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+ } else {
+ return -1;
+ }
+ } else if (rhs instanceof RecordedProgram) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ private final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ if (lhs instanceof ScheduledRecording) {
+ if (rhs instanceof ScheduledRecording) {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+ } else {
+ return -1;
+ }
+ } else if (rhs instanceof ScheduledRecording) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ private final TvInputCallback mTvInputCallback = new TvInputCallback() {
+ @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);
+ }
+ }
+ 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();
+ }
+ };
+
+ private final Runnable mUpdateRowsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateRows();
+ }
+ };
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
- mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
+ Context context = getContext();
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper();
+ mPresenterSelector = new ClassPresenterSelector()
+ .addClassPresenter(ScheduledRecording.class,
+ new ScheduledRecordingPresenter(context))
+ .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context))
+ .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context))
+ .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter());
+ mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
+ mGenreLabels.add(getString(R.string.dvr_main_others));
setupUiElements();
setupAdapters();
- mRecordingsInProgressAdapter.start();
- mRecordingsNotStatedAdapter.start();
- mRecordedProgramsAdapter.start();
- initRows();
+ mTvInputManagerHelper.addCallback(mTvInputCallback);
prepareEntranceTransition();
- startEntranceTransition();
- }
-
- @Override
- public void onStart() {
- if (DEBUG) Log.d(TAG, "onStart");
- super.onStart();
- // TODO: It's a workaround for a bug that a progress bar isn't hidden.
- // We need to remove it later.
- getProgressBarManager().disableProgressBar();
+ if (mDvrDataManager.isInitialized()) {
+ startEntranceTransition();
+ } else {
+ if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
+ mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
+ }
+ if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
+ mDvrDataManager.addRecordedProgramLoadFinishedListener(this);
+ }
+ }
}
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
- mRecordingsInProgressAdapter.stop();
- mRecordingsNotStatedAdapter.stop();
- mRecordedProgramsAdapter.stop();
super.onDestroy();
+ mHandler.removeCallbacks(mUpdateRowsRunnable);
+ mTvInputManagerHelper.removeCallback(mTvInputCallback);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrDataManager.removeSeriesRecordingListener(this);
+ mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ mRowsAdapter.clear();
+ mSeriesId2LatestProgram.clear();
+ }
+
+ @Override
+ public void onDvrScheduleLoadFinished() {
+ List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings();
+ onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings));
+ List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings();
+ onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings));
+ if (mDvrDataManager.isInitialized()) {
+ startEntranceTransition();
+ }
+ mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
+ }
+
+ @Override
+ public void onRecordedProgramLoadFinished() {
+ for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
+ if (isInputExist(recordedProgram.getInputId())) {
+ handleRecordedProgramAdded(recordedProgram, true);
+ }
+ }
+ updateRows();
+ if (mDvrDataManager.isInitialized()) {
+ startEntranceTransition();
+ }
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ }
+
+ @Override
+ public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
+ if (isInputExist(recordedProgram.getInputId())) {
+ handleRecordedProgramAdded(recordedProgram, true);
+ postUpdateRows();
+ }
+ }
+
+ @Override
+ public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ if (isInputExist(recordedProgram.getInputId())) {
+ handleRecordedProgramChanged(recordedProgram);
+ postUpdateRows();
+ }
+ }
+
+ @Override
+ public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ if (isInputExist(recordedProgram.getInputId())) {
+ handleRecordedProgramRemoved(recordedProgram);
+ postUpdateRows();
+ }
+ }
+
+ // No need to call updateRows() during ScheduledRecordings' change because
+ // the row for ScheduledRecordings is always displayed.
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ if (needToShowScheduledRecording(scheduleRecording)) {
+ mScheduleAdapter.add(scheduleRecording);
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ mScheduleAdapter.remove(scheduleRecording);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ if (needToShowScheduledRecording(scheduleRecording)) {
+ mScheduleAdapter.change(scheduleRecording);
+ } else {
+ mScheduleAdapter.removeWithId(scheduleRecording);
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ // Workaround of b/29108300
+ @Override
+ public void showTitle(int flags) {
+ flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE;
+ super.showTitle(flags);
}
private void setupUiElements() {
+ setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(false);
+ setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
}
private void setupAdapters() {
+ mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
+ mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
+ mSeriesAdapter = new RecordedProgramAdapter();
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ mGenreAdapters[i] = new RecordedProgramAdapter();
+ }
+ // Schedule Recordings.
+ List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
+ onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
+ mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
+ // Recorded Programs.
+ for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
+ if (isInputExist(recordedProgram.getInputId())) {
+ handleRecordedProgramAdded(recordedProgram, false);
+ }
+ }
+ // Series Recordings. Series recordings should be added after recorded programs, because
+ // we build series recordings' latest program information while adding recorded programs.
+ List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
+ handleSeriesRecordingsAdded(recordings);
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+ mRecentRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_recent)), mRecentAdapter);
+ mRowsAdapter.add(new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_scheduled)), mScheduleAdapter));
+ mSeriesRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_series)), mSeriesAdapter);
+ updateRows();
+ mDvrDataManager.addRecordedProgramListener(this);
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addSeriesRecordingListener(this);
setAdapter(mRowsAdapter);
- ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
- EmptyItemPresenter emptyItemPresenter = new EmptyItemPresenter(this);
- ScheduledRecordingPresenter scheduledRecordingPresenter = new ScheduledRecordingPresenter(
- getContext());
- RecordedProgramPresenter recordedProgramPresenter = new RecordedProgramPresenter(
- getContext());
- presenterSelector.addClassPresenter(ScheduledRecording.class, scheduledRecordingPresenter);
- presenterSelector.addClassPresenter(RecordedProgram.class, recordedProgramPresenter);
- presenterSelector.addClassPresenter(EmptyHolder.class, emptyItemPresenter);
- mRecordingsInProgressAdapter = new ScheduledRecordingsAdapter(mDvrDataManager,
- ScheduledRecording.STATE_RECORDING_IN_PROGRESS, presenterSelector);
- mRecordingsNotStatedAdapter = new ScheduledRecordingsAdapter(mDvrDataManager,
- ScheduledRecording.STATE_RECORDING_NOT_STARTED, presenterSelector);
- mRecordedProgramsAdapter = new RecordedProgramsAdapter(mDvrDataManager, presenterSelector);
- }
-
- private void initRows() {
- mRowsAdapter.clear();
- for (@DVR_HEADERS_MODE int i : sHeaders.keySet()) {
- HeaderItem gridHeader = new HeaderItem(i, getContext().getString(sHeaders.get(i)));
- ObjectAdapter gridRowAdapter = null;
- switch (i) {
- case DVR_CURRENT_RECORDINGS: {
- gridRowAdapter = mRecordingsInProgressAdapter;
- break;
+ }
+
+ private void handleRecordedProgramAdded(RecordedProgram recordedProgram,
+ boolean updateSeriesRecording) {
+ mRecentAdapter.add(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ SeriesRecording seriesRecording = null;
+ if (seriesId != null) {
+ seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
+ if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .compare(latestProgram, recordedProgram) < 0) {
+ mSeriesId2LatestProgram.put(seriesId, recordedProgram);
+ if (updateSeriesRecording && seriesRecording != null) {
+ onSeriesRecordingChanged(seriesRecording);
+ }
+ }
+ }
+ if (seriesRecording == null) {
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(recordedProgram.getCanonicalGenres())) {
+ adapter.add(recordedProgram);
+ }
+ }
+ }
+
+ private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ mRecentAdapter.remove(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ if (seriesId != null) {
+ SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram =
+ mSeriesId2LatestProgram.get(recordedProgram.getSeriesId());
+ if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) {
+ if (seriesRecording != null) {
+ updateLatestRecordedProgram(seriesRecording);
+ onSeriesRecordingChanged(seriesRecording);
}
- case DVR_SCHEDULED_RECORDINGS: {
- gridRowAdapter = mRecordingsNotStatedAdapter;
+ }
+ }
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(recordedProgram.getCanonicalGenres())) {
+ adapter.remove(recordedProgram);
+ }
+ }
+
+ private void handleRecordedProgramChanged(RecordedProgram recordedProgram) {
+ mRecentAdapter.change(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ SeriesRecording seriesRecording = null;
+ if (seriesId != null) {
+ seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
+ if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .compare(latestProgram, recordedProgram) <= 0) {
+ mSeriesId2LatestProgram.put(seriesId, recordedProgram);
+ if (seriesRecording != null) {
+ onSeriesRecordingChanged(seriesRecording);
}
- break;
- case DVR_RECORDED_PROGRAMS: {
- gridRowAdapter = mRecordedProgramsAdapter;
+ } else if (latestProgram.getId() == recordedProgram.getId()) {
+ if (seriesRecording != null) {
+ updateLatestRecordedProgram(seriesRecording);
+ onSeriesRecordingChanged(seriesRecording);
}
- break;
- case DVR_SETTINGS:
- gridRowAdapter = new ArrayObjectAdapter(new EmptyItemPresenter(this));
- // TODO: provide setup rows.
- break;
}
- if (gridRowAdapter != null) {
- mRowsAdapter.add(new ListRow(gridHeader, gridRowAdapter));
+ }
+ if (seriesRecording == null) {
+ updateGenreAdapters(getGenreAdapters(
+ recordedProgram.getCanonicalGenres()), recordedProgram);
+ } else {
+ updateGenreAdapters(new ArrayList<>(), recordedProgram);
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+ }
+
+ private void handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ mSeriesAdapter.remove(seriesRecording);
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
+ adapter.remove(seriesRecording);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) {
+ List<RecordedProgramAdapter> result = new ArrayList<>();
+ if (genres == null || genres.length == 0) {
+ result.add(mGenreAdapters[mGenreAdapters.length - 1]);
+ } else {
+ for (String genre : genres) {
+ int genreId = GenreItems.getId(genre);
+ if(genreId >= mGenreAdapters.length) {
+ Log.d(TAG, "Wrong Genre ID: " + genreId);
+ } else {
+ result.add(mGenreAdapters[genreId]);
+ }
+ }
+ }
+ return result;
+ }
+
+ private List<RecordedProgramAdapter> getGenreAdapters(int[] genreIds) {
+ List<RecordedProgramAdapter> result = new ArrayList<>();
+ if (genreIds == null || genreIds.length == 0) {
+ result.add(mGenreAdapters[mGenreAdapters.length - 1]);
+ } else {
+ for (int genreId : genreIds) {
+ if(genreId >= mGenreAdapters.length) {
+ Log.d(TAG, "Wrong Genre ID: " + genreId);
+ } else {
+ result.add(mGenreAdapters[genreId]);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void updateGenreAdapters(List<RecordedProgramAdapter> adapters, Object r) {
+ for (RecordedProgramAdapter adapter : mGenreAdapters) {
+ if (adapters.contains(adapter)) {
+ adapter.change(r);
+ } else {
+ adapter.remove(r);
+ }
+ }
+ }
+
+ private void postUpdateRows() {
+ mHandler.removeCallbacks(mUpdateRowsRunnable);
+ mHandler.post(mUpdateRowsRunnable);
+ }
+
+ private void updateRows() {
+ int visibleRowsCount = 1; // Schedule's Row will never be empty
+ if (mRecentAdapter.isEmpty()) {
+ mRowsAdapter.remove(mRecentRow);
+ } else {
+ if (mRowsAdapter.indexOf(mRecentRow) < 0) {
+ mRowsAdapter.add(0, mRecentRow);
+ }
+ visibleRowsCount++;
+ }
+ if (mSeriesAdapter.isEmpty()) {
+ mRowsAdapter.remove(mSeriesRow);
+ } else {
+ if (mRowsAdapter.indexOf(mSeriesRow) < 0) {
+ mRowsAdapter.add(visibleRowsCount, mSeriesRow);
+ }
+ visibleRowsCount++;
+ }
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ RecordedProgramAdapter adapter = mGenreAdapters[i];
+ if (adapter != null) {
+ if (adapter.isEmpty()) {
+ mRowsAdapter.remove(mGenreRows[i]);
+ } else {
+ if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) {
+ mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter);
+ mRowsAdapter.add(visibleRowsCount, mGenreRows[i]);
+ }
+ visibleRowsCount++;
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+
+ 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)) {
+ latestProgram = program;
+ }
+ }
+ mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram);
+ }
+
+ private class ScheduleAdapter extends SortedArrayAdapter<Object> {
+ ScheduleAdapter(int maxItemCount) {
+ super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount);
+ }
+
+ @Override
+ public long getId(Object item) {
+ if (item instanceof ScheduledRecording) {
+ return ((ScheduledRecording) item).getId();
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
+ RecordedProgramAdapter() {
+ this(Integer.MAX_VALUE);
+ }
+
+ RecordedProgramAdapter(int maxItemCount) {
+ super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount);
+ }
+
+ @Override
+ public long getId(Object item) {
+ if (item instanceof SeriesRecording) {
+ return ((SeriesRecording) item).getId();
+ } else if (item instanceof RecordedProgram) {
+ return ((RecordedProgram) item).getId();
+ } else {
+ return -1;
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java
new file mode 100644
index 00000000..d1cf57a6
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.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.DialogFragment;
+import android.os.Bundle;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+
+/**
+ * A dialog fragment which contains {@link DvrCancelAllSeriesRecordingFragment}.
+ */
+public class DvrCancelAllSeriesRecordingDialogFragment 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();
+ fragment.setArguments(getArguments());
+ GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ return view;
+ }
+
+ @Override
+ public int getTheme() {
+ return R.style.Theme_TV_dialog_HalfSizedDialog;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java
new file mode 100644
index 00000000..78f73fd5
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java
@@ -0,0 +1,65 @@
+/*
+ * 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
new file mode 100644
index 00000000..fe65eebd
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
@@ -0,0 +1,111 @@
+/*
+ * 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.graphics.drawable.Drawable;
+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 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;
+
+import java.util.ArrayList;
+import java.util.List;
+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) {
+ Bundle args = getArguments();
+ if (args != null) {
+ long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
+ mChannel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(channelId);
+ }
+ SoftPreconditions.checkArgument(mChannel != null);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_channel_record_duration_dialog_title);
+ Drawable icon = getResources().getDrawable(R.drawable.ic_dvr, null);
+ return new Guidance(title, null, null, icon);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ int actionId = -1;
+ mDurations.clear();
+ mDurations.add(TimeUnit.MINUTES.toMillis(10));
+ mDurations.add(TimeUnit.MINUTES.toMillis(30));
+ mDurations.add(TimeUnit.HOURS.toMillis(1));
+ mDurations.add(TimeUnit.HOURS.toMillis(3));
+
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(++actionId)
+ .title(R.string.recording_start_dialog_10_min_duration)
+ .build());
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(++actionId)
+ .title(R.string.recording_start_dialog_30_min_duration)
+ .build());
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(++actionId)
+ .title(R.string.recording_start_dialog_1_hour_duration)
+ .build());
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(++actionId)
+ .title(R.string.recording_start_dialog_3_hours_duration)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ long duration = mDurations.get((int) action.getId());
+ long startTimeMs = System.currentTimeMillis();
+ long endTimeMs = System.currentTimeMillis() + duration;
+ List<ScheduledRecording> conflicts = dvrManager.getConflictingSchedules(
+ mChannel.getId(), startTimeMs, endTimeMs);
+ dvrManager.addSchedule(mChannel, startTimeMs, endTimeMs);
+ if (conflicts.isEmpty()) {
+ dismissDialog();
+ } else {
+ GuidedStepFragment fragment = new DvrChannelRecordConflictFragment();
+ Bundle args = new Bundle();
+ args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, mChannel.getId());
+ args.putLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS, startTimeMs);
+ args.putLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS, endTimeMs);
+ fragment.setArguments(args);
+ GuidedStepFragment.add(getFragmentManager(), fragment,
+ R.id.halfsized_dialog_host);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
new file mode 100644
index 00000000..e7be4d0a
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -0,0 +1,339 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.media.tv.TvInputInfo;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.util.Log;
+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.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.ConflictChecker;
+import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
+ private static final String TAG = "DvrConflictFragment";
+ private static final boolean DEBUG = false;
+
+ private static final int ACTION_DELETE_CONFLICT = 1;
+ private static final int ACTION_CANCEL = 2;
+ private static final int ACTION_VIEW_SCHEDULES = 3;
+
+ // The program count which will be listed in the description. This is the number of the
+ // program strings in R.plurals.dvr_program_conflict_dialog_description_many.
+ private static final int LISTED_PROGRAM_COUNT = 2;
+
+ protected List<ScheduledRecording> mConflicts;
+
+ void setConflicts(List<ScheduledRecording> conflicts) {
+ mConflicts = conflicts;
+ }
+
+ List<ScheduledRecording> getConflicts() {
+ return mConflicts;
+ }
+
+ @Override
+ public int onProvideTheme() {
+ return R.style.Theme_TV_Dvr_Conflict_GuidedStep;
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getContext())
+ .clickAction(GuidedAction.ACTION_ID_OK)
+ .build());
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(ACTION_VIEW_SCHEDULES)
+ .title(R.string.dvr_action_view_schedules)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_VIEW_SCHEDULES) {
+ DvrUiHelper.startSchedulesActivityForOneTimeRecordingConflict(
+ getContext(), getConflicts());
+ }
+ dismissDialog();
+ }
+
+ String getConflictDescription() {
+ List<String> titles = new ArrayList<>();
+ HashSet<String> titleSet = new HashSet<>();
+ for (ScheduledRecording schedule : getConflicts()) {
+ String scheduleTitle = getScheduleTitle(schedule);
+ if (scheduleTitle != null && !titleSet.contains(scheduleTitle)) {
+ titles.add(scheduleTitle);
+ titleSet.add(scheduleTitle);
+ }
+ }
+ switch (titles.size()) {
+ case 0:
+ Log.i(TAG, "Conflict has been resolved by any reason. Maybe input might have"
+ + " been deleted.");
+ return null;
+ case 1:
+ return getResources().getString(
+ R.string.dvr_program_conflict_dialog_description_1, titles.get(0));
+ case 2:
+ return getResources().getString(
+ R.string.dvr_program_conflict_dialog_description_2, titles.get(0),
+ titles.get(1));
+ case 3:
+ return getResources().getString(
+ R.string.dvr_program_conflict_dialog_description_3, titles.get(0),
+ titles.get(1));
+ default:
+ return getResources().getQuantityString(
+ R.plurals.dvr_program_conflict_dialog_description_many,
+ titles.size() - LISTED_PROGRAM_COUNT, titles.get(0), titles.get(1),
+ titles.size() - LISTED_PROGRAM_COUNT);
+ }
+ }
+
+ @Nullable
+ private String getScheduleTitle(ScheduledRecording schedule) {
+ if (schedule.getType() == ScheduledRecording.TYPE_TIMED) {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(schedule.getChannelId());
+ if (channel != null) {
+ return channel.getDisplayName();
+ } else {
+ return null;
+ }
+ } else {
+ return schedule.getProgramTitle();
+ }
+ }
+
+ /**
+ * A fragment to show the program conflict.
+ */
+ public static class DvrProgramConflictFragment extends DvrConflictFragment {
+ private Program mProgram;
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
+ }
+ SoftPreconditions.checkArgument(mProgram != null);
+ TvInputInfo input = Utils.getTvInputInfoForProgram(getContext(), mProgram);
+ SoftPreconditions.checkNotNull(input);
+ List<ScheduledRecording> conflicts = null;
+ if (input != null) {
+ conflicts = TvApplication.getSingletons(getContext()).getDvrManager()
+ .getConflictingSchedules(mProgram);
+ }
+ if (conflicts == null) {
+ conflicts = Collections.emptyList();
+ }
+ if (conflicts.isEmpty()) {
+ dismissDialog();
+ }
+ setConflicts(conflicts);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_program_conflict_dialog_title);
+ String descriptionPrefix = getString(
+ R.string.dvr_program_conflict_dialog_description_prefix, mProgram.getTitle());
+ String description = getConflictDescription();
+ if (description == null) {
+ dismissDialog();
+ }
+ Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ return new Guidance(title, descriptionPrefix + " " + description, null, icon);
+ }
+ }
+
+ /**
+ * A fragment to show the channel recording conflict.
+ */
+ public static class DvrChannelRecordConflictFragment extends DvrConflictFragment {
+ private Channel mChannel;
+ private long mStartTimeMs;
+ private long mEndTimeMs;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
+ mChannel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(channelId);
+ SoftPreconditions.checkArgument(mChannel != null);
+ TvInputInfo input = Utils.getTvInputInfoForChannelId(getContext(), mChannel.getId());
+ SoftPreconditions.checkNotNull(input);
+ List<ScheduledRecording> conflicts = null;
+ if (input != null) {
+ mStartTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS);
+ mEndTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS);
+ conflicts = TvApplication.getSingletons(getContext()).getDvrManager()
+ .getConflictingSchedules(mChannel.getId(), mStartTimeMs, mEndTimeMs);
+ }
+ if (conflicts == null) {
+ conflicts = Collections.emptyList();
+ }
+ if (conflicts.isEmpty()) {
+ dismissDialog();
+ }
+ setConflicts(conflicts);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_channel_conflict_dialog_title);
+ String descriptionPrefix = getString(
+ R.string.dvr_channel_conflict_dialog_description_prefix,
+ mChannel.getDisplayName());
+ String description = getConflictDescription();
+ if (description == null) {
+ dismissDialog();
+ }
+ Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ return new Guidance(title, descriptionPrefix + " " + description, null, icon);
+ }
+ }
+
+ /**
+ * A fragment to show the channel watching conflict.
+ * <p>
+ * This fragment is automatically closed when there are no upcoming conflicts.
+ */
+ public static class DvrChannelWatchConflictFragment extends DvrConflictFragment
+ implements OnUpcomingConflictChangeListener {
+ private long mChannelId;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mChannelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
+ }
+ SoftPreconditions.checkArgument(mChannelId != Channel.INVALID_ID);
+ ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
+ List<ScheduledRecording> conflicts = null;
+ if (checker != null) {
+ checker.addOnUpcomingConflictChangeListener(this);
+ conflicts = checker.getUpcomingConflicts();
+ if (DEBUG) Log.d(TAG, "onCreateView: upcoming conflicts: " + conflicts);
+ if (conflicts.isEmpty()) {
+ dismissDialog();
+ }
+ }
+ if (conflicts == null) {
+ if (DEBUG) Log.d(TAG, "onCreateView: There's no conflict.");
+ conflicts = Collections.emptyList();
+ }
+ if (conflicts.isEmpty()) {
+ dismissDialog();
+ }
+ setConflicts(conflicts);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(
+ R.string.dvr_epg_channel_watch_conflict_dialog_title);
+ String description = getResources().getString(
+ R.string.dvr_epg_channel_watch_conflict_dialog_description);
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(ACTION_DELETE_CONFLICT)
+ .title(R.string.dvr_action_delete_schedule)
+ .build());
+ actions.add(new GuidedAction.Builder(getContext())
+ .id(ACTION_CANCEL)
+ .title(R.string.dvr_action_record_program)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_CANCEL) {
+ ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
+ if (checker != null) {
+ checker.setCheckedConflictsForChannel(mChannelId, getConflicts());
+ }
+ } else if (action.getId() == ACTION_DELETE_CONFLICT) {
+ for (ScheduledRecording schedule : mConflicts) {
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ getDvrManager().stopRecording(schedule);
+ } else {
+ getDvrManager().removeScheduledRecording(schedule);
+ }
+ }
+ }
+ super.onGuidedActionClicked(action);
+ }
+
+ @Override
+ public void onDetach() {
+ ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
+ if (checker != null) {
+ checker.removeOnUpcomingConflictChangeListener(this);
+ }
+ super.onDetach();
+ }
+
+ @Override
+ public void onUpcomingConflictChange() {
+ ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
+ if (checker == null || checker.getUpcomingConflicts().isEmpty()) {
+ if (DEBUG) Log.d(TAG, "onUpcomingConflictChange: There's no conflict.");
+ dismissDialog();
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
new file mode 100644
index 00000000..b273c85c
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
@@ -0,0 +1,96 @@
+/*
+ * 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.DetailsFragment;
+
+import com.android.tv.R;
+
+/**
+ * Activity to show details view in DVR.
+ */
+public class DvrDetailsActivity extends Activity {
+ /**
+ * Name of record id added to the Intent.
+ */
+ public static final String RECORDING_ID = "record_id";
+
+ /**
+ * Name of flag added to the Intent to determine if details view should hide "View schedule"
+ * button.
+ */
+ public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
+
+ /**
+ * Name of details view's type added to the intent.
+ */
+ public static final String DETAILS_VIEW_TYPE = "details_view_type";
+
+ /**
+ * Name of shared element between activities.
+ */
+ public static final String SHARED_ELEMENT_NAME = "shared_element";
+
+ /**
+ * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR.
+ */
+ public static final int CURRENT_RECORDING_VIEW = 1;
+
+ /**
+ * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR.
+ */
+ public static final int SCHEDULED_RECORDING_VIEW = 2;
+
+ /**
+ * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR.
+ */
+ public static final int RECORDED_PROGRAM_VIEW = 3;
+
+ /**
+ * SERIES_RECORDING_VIEW refers to series recording in DVR.
+ */
+ public static final int SERIES_RECORDING_VIEW = 4;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_details);
+ long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
+ int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
+ boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+ if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) {
+ Bundle args = new Bundle();
+ args.putLong(RECORDING_ID, recordId);
+ DetailsFragment detailsFragment = null;
+ if (detailsViewType == CURRENT_RECORDING_VIEW) {
+ detailsFragment = new CurrentRecordingDetailsFragment();
+ } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
+ args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
+ detailsFragment = new ScheduledRecordingDetailsFragment();
+ } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
+ detailsFragment = new RecordedProgramDetailsFragment();
+ } else if (detailsViewType == SERIES_RECORDING_VIEW) {
+ detailsFragment = new SeriesRecordingDetailsFragment();
+ }
+ detailsFragment.setArguments(args);
+ getFragmentManager().beginTransaction()
+ .replace(R.id.dvr_details_view_frame, detailsFragment).commit();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
new file mode 100644
index 00000000..be995fcb
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
@@ -0,0 +1,241 @@
+/*
+ * 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.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+
+import com.android.tv.R;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.Channel;
+import com.android.tv.util.ImageLoader;
+
+abstract class DvrDetailsFragment extends DetailsFragment {
+ private static final int LOAD_LOGO_IMAGE = 1;
+ private static final int LOAD_BACKGROUND_IMAGE = 2;
+
+ protected DetailsViewBackgroundHelper mBackgroundHelper;
+ private ArrayObjectAdapter mRowsAdapter;
+ private DetailsOverviewRow mDetailsOverview;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!onLoadRecordingDetails(getArguments())) {
+ getActivity().finish();
+ return;
+ }
+ mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
+ setupAdapter();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // TODO: remove the workaround of b/30401180.
+ VerticalGridView container = (VerticalGridView) getActivity()
+ .findViewById(R.id.container_list);
+ // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout.
+ container.setItemAlignmentOffset(0);
+ container.setWindowAlignmentOffset(
+ getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top));
+ }
+
+ private void setupAdapter() {
+ DetailsOverviewRowPresenter rowPresenter =
+ new DetailsOverviewRowPresenter(new DetailsContentPresenter());
+ rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background,
+ null));
+ rowPresenter.setSharedElementEnterTransition(getActivity(),
+ DvrDetailsActivity.SHARED_ELEMENT_NAME);
+ rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
+ mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
+ setAdapter(mRowsAdapter);
+ }
+
+ /**
+ * Returns details views' rows adapter.
+ */
+ protected ArrayObjectAdapter getRowsAdapter() {
+ return mRowsAdapter;
+ }
+
+ /**
+ * Sets details overview.
+ */
+ protected void setDetailsOverviewRow(DetailsContent detailsContent) {
+ mDetailsOverview = new DetailsOverviewRow(detailsContent);
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ mRowsAdapter.add(mDetailsOverview);
+ onLoadLogoAndBackgroundImages(detailsContent);
+ }
+
+ /**
+ * Creates and returns presenter selector will be used by rows adaptor.
+ */
+ protected PresenterSelector onCreatePresenterSelector(DetailsOverviewRowPresenter rowPresenter) {
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+ return presenterSelector;
+ }
+
+ /**
+ * Updates actions of details overview.
+ */
+ protected void updateActions() {
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ }
+
+ /**
+ * Loads recording details according to the arguments the fragment got.
+ *
+ * @return false if cannot find valid recordings, else return true. If the return value
+ * is false, the detail activity and fragment will be ended.
+ */
+ abstract boolean onLoadRecordingDetails(Bundle args);
+
+ /**
+ * Creates actions users can interact with and their adaptor for this fragment.
+ */
+ abstract SparseArrayObjectAdapter onCreateActionsAdapter();
+
+ /**
+ * Creates actions listeners to implement the behavior of the fragment after users click some
+ * action buttons.
+ */
+ abstract OnActionClickedListener onCreateOnActionClickedListener();
+
+ /**
+ * Returns program title with episode number. If the program is null, returns channel name.
+ */
+ protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) {
+ String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext());
+ SpannableString title = titleWithEpisodeNumber == null ? null
+ : new SpannableString(titleWithEpisodeNumber);
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : getContext().getResources().getString(
+ R.string.no_program_information));
+ } else {
+ String programTitle = program.getTitle();
+ title.setSpan(new TextAppearanceSpan(getContext(),
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return title;
+ }
+
+ /**
+ * Loads logo and background images for detail fragments.
+ */
+ protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) {
+ Drawable logoDrawable = null;
+ Drawable backgroundDrawable = null;
+ if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) {
+ logoDrawable = getContext().getResources()
+ .getDrawable(R.drawable.dvr_default_poster, null);
+ mDetailsOverview.setImageDrawable(logoDrawable);
+ }
+ if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) {
+ backgroundDrawable = getContext().getResources()
+ .getDrawable(R.drawable.dvr_default_poster, null);
+ mBackgroundHelper.setBackground(backgroundDrawable);
+ }
+ if (logoDrawable != null && backgroundDrawable != null) {
+ return;
+ }
+ if (logoDrawable == null && backgroundDrawable == null
+ && detailsContent.getLogoImageUri().equals(
+ detailsContent.getBackgroundImageUri())) {
+ ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(),
+ new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE,
+ getContext()));
+ return;
+ }
+ if (logoDrawable == null) {
+ int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width);
+ int imageHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_details_poster_height);
+ ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(),
+ imageWidth, imageHeight,
+ new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext()));
+ }
+ if (backgroundDrawable == null) {
+ ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(),
+ new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext()));
+ }
+ }
+
+ private static class MyImageLoaderCallback extends
+ ImageLoader.ImageLoaderCallback<DvrDetailsFragment> {
+ private final Context mContext;
+ private final int mLoadType;
+
+ public MyImageLoaderCallback(DvrDetailsFragment fragment,
+ int loadType, Context context) {
+ super(fragment);
+ mLoadType = loadType;
+ mContext = context;
+ }
+
+ @Override
+ public void onBitmapLoaded(DvrDetailsFragment fragment,
+ @Nullable Bitmap bitmap) {
+ Drawable drawable;
+ int loadType = mLoadType;
+ if (bitmap == null) {
+ Resources res = mContext.getResources();
+ drawable = res.getDrawable(R.drawable.dvr_default_poster, null);
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) {
+ loadType &= ~LOAD_BACKGROUND_IMAGE;
+ fragment.mBackgroundHelper.setBackgroundColor(
+ res.getColor(R.color.dvr_detail_default_background));
+ fragment.mBackgroundHelper.setScrim(
+ res.getColor(R.color.dvr_detail_default_background_scrim));
+ }
+ } else {
+ drawable = new BitmapDrawable(mContext.getResources(), bitmap);
+ }
+ if (!fragment.isDetached()) {
+ if ((loadType & LOAD_LOGO_IMAGE) != 0) {
+ fragment.mDetailsOverview.setImageDrawable(drawable);
+ }
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) {
+ fragment.mBackgroundHelper.setBackground(drawable);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrDialogFragment.java b/src/com/android/tv/dvr/ui/DvrDialogFragment.java
deleted file mode 100644
index 38de9d8d..00000000
--- a/src/com/android/tv/dvr/ui/DvrDialogFragment.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.android.tv.dvr.ui;
-
-import android.app.FragmentManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
-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.guide.ProgramGuide;
-
-public class DvrDialogFragment extends HalfSizedDialogFragment {
- private final DvrGuidedStepFragment mDvrGuidedStepFragment;
-
- public DvrDialogFragment(DvrGuidedStepFragment dvrGuidedStepFragment) {
- mDvrGuidedStepFragment = dvrGuidedStepFragment;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- ProgramGuide programGuide =
- ((MainActivity) getActivity()).getOverlayManager().getProgramGuide();
- if (programGuide != null && programGuide.isActive()) {
- programGuide.cancelHide();
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = super.onCreateView(inflater, container, savedInstanceState);
- FragmentManager fm = getChildFragmentManager();
- GuidedStepFragment.add(fm, mDvrGuidedStepFragment, R.id.halfsized_dialog_host);
- return view;
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- ProgramGuide programGuide =
- ((MainActivity) getActivity()).getOverlayManager().getProgramGuide();
- if (programGuide != null && programGuide.isActive()) {
- programGuide.scheduleHide();
- }
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
new file mode 100644
index 00000000..6f287c70
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+
+import java.util.List;
+
+public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_CANCEL = 1;
+ private static final int ACTION_FORGET_STORAGE = 2;
+ private String mInputId;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID);
+ }
+ SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId));
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_error_forget_storage_title);
+ String description = getResources().getString(
+ R.string.dvr_error_forget_storage_description);
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_CANCEL)
+ .title(getResources().getString(R.string.dvr_action_error_cancel))
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_FORGET_STORAGE)
+ .title(getResources().getString(R.string.dvr_action_error_forget_storage))
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() != ACTION_FORGET_STORAGE) {
+ dismissDialog();
+ return;
+ }
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ 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);
+ Activity activity = getActivity();
+ if (activity instanceof DvrDetailsActivity) {
+ // Since we removed everything, just finish the activity.
+ activity.finish();
+ } else {
+ dismissDialog();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java
new file mode 100644
index 00000000..6b0c22ff
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrGuidedActionsStylist.java
@@ -0,0 +1,78 @@
+/*
+ * 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.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.android.tv.R;
+
+/**
+ * Stylist class used for DVR settings {@link GuidedStepFragment}.
+ */
+public class DvrGuidedActionsStylist extends GuidedActionsStylist {
+ private static boolean sInitialized;
+ private static float sWidthWeight;
+ private static int sItemHeight;
+
+ private final boolean mIsButtonActions;
+
+ public DvrGuidedActionsStylist(boolean isButtonActions) {
+ super();
+ mIsButtonActions = isButtonActions;
+ if (mIsButtonActions) {
+ setAsButtonActions();
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container) {
+ initializeIfNeeded(container.getContext());
+ View v = super.onCreateView(inflater, container);
+ if (mIsButtonActions) {
+ ((LinearLayout.LayoutParams) v.getLayoutParams()).weight = sWidthWeight;
+ }
+ return v;
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ initializeIfNeeded(parent.getContext());
+ ViewHolder viewHolder = super.onCreateViewHolder(parent);
+ viewHolder.itemView.getLayoutParams().height = sItemHeight;
+ return viewHolder;
+ }
+
+ private void initializeIfNeeded(Context context) {
+ if (sInitialized) {
+ return;
+ }
+ sInitialized = true;
+ sItemHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.dvr_settings_one_line_action_container_height);
+ TypedValue outValue = new TypedValue();
+ context.getResources().getValue(R.dimen.dvr_settings_button_actions_list_width_weight,
+ outValue, true);
+ sWidthWeight = outValue.getFloat();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index 0854b91a..eaccd8ed 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -1,33 +1,39 @@
+/*
+ * 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.DialogFragment;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
-import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.VerticalGridView;
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.TvApplication;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.guide.ProgramManager.TableEntry;
-import com.android.tv.R;
public class DvrGuidedStepFragment extends GuidedStepFragment {
- private final TableEntry mEntry;
private DvrManager mDvrManager;
- public DvrGuidedStepFragment(TableEntry entry) {
- mEntry = entry;
- }
-
- protected TableEntry getEntry() {
- return mEntry;
- }
-
protected DvrManager getDvrManager() {
return mDvrManager;
}
@@ -42,32 +48,27 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
- VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
- gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
+ VerticalGridView actionsList = getGuidedActionsStylist().getActionsGridView();
+ actionsList.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
+ VerticalGridView buttonActionsList = getGuidedButtonActionsStylist().getActionsGridView();
+ buttonActionsList.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_BOTH_EDGE);
return view;
}
@Override
- public GuidanceStylist onCreateGuidanceStylist() {
- // Workaround: b/28448653
- return new GuidanceStylist() {
- @Override
- public int onProvideLayoutId() {
- return R.layout.halfsized_guidance;
- }
- };
- }
-
- @Override
public int onProvideTheme() {
return R.style.Theme_TV_Dvr_GuidedStep;
}
protected void dismissDialog() {
- SafeDismissDialogFragment currentDialog =
- ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
- if (currentDialog instanceof DvrDialogFragment) {
- currentDialog.dismiss();
+ if (getActivity() instanceof MainActivity) {
+ SafeDismissDialogFragment currentDialog =
+ ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
+ if (currentDialog instanceof DvrHalfSizedDialogFragment) {
+ currentDialog.dismiss();
+ }
+ } else if (getParentFragment() instanceof DialogFragment) {
+ ((DialogFragment) getParentFragment()).dismiss();
}
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
new file mode 100644
index 00000000..50187a56
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -0,0 +1,180 @@
+/*
+ * 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.content.Context;
+import android.os.Bundle;
+import android.support.v17.leanback.app.GuidedStepFragment;
+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.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
+import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
+import com.android.tv.guide.ProgramGuide;
+
+public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
+ /**
+ * Key for input ID.
+ * Type: String.
+ */
+ public static final String KEY_INPUT_ID = "DvrHalfSizedDialogFragment.input_id";
+ /**
+ * Key for the program.
+ * Type: {@link com.android.tv.data.Program}.
+ */
+ 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.
+ */
+ public static final String KEY_CHANNEL_ID = "DvrHalfSizedDialogFragment.channel_id";
+ /**
+ * Key for the recording start time in millisecond.
+ * Type: long.
+ */
+ public static final String KEY_START_TIME_MS = "DvrHalfSizedDialogFragment.start_time_ms";
+ /**
+ * Key for the recording end time in millisecond.
+ * Type: long.
+ */
+ public static final String KEY_END_TIME_MS = "DvrHalfSizedDialogFragment.end_time_ms";
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Activity activity = getActivity();
+ if (activity instanceof MainActivity) {
+ ProgramGuide programGuide =
+ ((MainActivity) activity).getOverlayManager().getProgramGuide();
+ if (programGuide != null && programGuide.isActive()) {
+ programGuide.cancelHide();
+ }
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ Activity activity = getActivity();
+ if (activity instanceof MainActivity) {
+ ProgramGuide programGuide =
+ ((MainActivity) activity).getOverlayManager().getProgramGuide();
+ if (programGuide != null && programGuide.isActive()) {
+ programGuide.scheduleHide();
+ }
+ }
+ }
+
+ public abstract static class DvrGuidedStepDialogFragment extends DvrHalfSizedDialogFragment {
+ @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);
+ return view;
+ }
+
+ protected abstract GuidedStepFragment onCreateGuidedStepFragment();
+ }
+
+ /** A dialog fragment for {@link DvrScheduleFragment}. */
+ public static class DvrScheduleDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrScheduleFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrProgramConflictFragment}. */
+ public static class DvrProgramConflictDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrProgramConflictFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrChannelWatchConflictFragment}. */
+ public static class DvrChannelWatchConflictDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrChannelWatchConflictFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrChannelRecordDurationOptionFragment}. */
+ public static class DvrChannelRecordDurationOptionDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrChannelRecordDurationOptionFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrInsufficientSpaceErrorFragment}. */
+ public static class DvrInsufficientSpaceErrorDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrInsufficientSpaceErrorFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrMissingStorageErrorFragment}. */
+ public static class DvrMissingStorageErrorDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrMissingStorageErrorFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrStopRecordingFragment}. */
+ public static class DvrStopRecordingDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrStopRecordingFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrAlreadyScheduledFragment}. */
+ public static class DvrAlreadyScheduledDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrAlreadyScheduledFragment();
+ }
+ }
+
+ /** A dialog fragment for {@link DvrAlreadyRecordedFragment}. */
+ public static class DvrAlreadyRecordedDialogFragment extends DvrGuidedStepDialogFragment {
+ @Override
+ protected GuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrAlreadyRecordedFragment();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
new file mode 100644
index 00000000..3b1dbfa0
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
@@ -0,0 +1,71 @@
+/*
+ * 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.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+
+import java.util.List;
+
+public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_DONE = 1;
+ private static final int ACTION_OPEN_DVR = 2;
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_error_insufficient_space_title);
+ String description = getResources()
+ .getString(R.string.dvr_error_insufficient_space_description);
+ 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(ACTION_DONE)
+ .title(getResources().getString(R.string.dvr_action_error_done))
+ .build());
+ DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext())
+ .getDvrDataManager();
+ if (!(dvrDataManager.getRecordedPrograms().isEmpty()
+ && dvrDataManager.getStartedRecordings().isEmpty()
+ && dvrDataManager.getNonStartedScheduledRecordings().isEmpty()
+ && dvrDataManager.getSeriesRecordings().isEmpty())) {
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_OPEN_DVR)
+ .title(getResources().getString(R.string.dvr_action_error_open_dvr))
+ .build());
+ }
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_OPEN_DVR) {
+ Intent intent = new Intent(getActivity(), DvrActivity.class);
+ getActivity().startActivity(intent);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
new file mode 100644
index 00000000..2e2c2849
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+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.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
+
+import java.util.List;
+
+public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_CANCEL = 1;
+ private static final int ACTION_FORGET_STORAGE = 2;
+ private String mInputId;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID);
+ }
+ SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId));
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(R.string.dvr_error_missing_storage_title);
+ String description = getResources().getString(
+ R.string.dvr_error_missing_storage_description);
+ 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(ACTION_CANCEL)
+ .title(getResources().getString(R.string.dvr_action_error_cancel))
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_FORGET_STORAGE)
+ .title(getResources().getString(R.string.dvr_action_error_forget_storage))
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_FORGET_STORAGE) {
+ DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment();
+ Bundle args = new Bundle();
+ args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId);
+ fragment.setArguments(args);
+ GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ return;
+ }
+ dismissDialog();
+ }
+} \ 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
new file mode 100644
index 00000000..7be92f1e
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java
@@ -0,0 +1,78 @@
+/*
+ * 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.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.DvrPlaybackActivity;
+import com.android.tv.util.Utils;
+
+/**
+ * This class is used to generate Views and bind Objects for related recordings in DVR playback.
+ */
+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;
+
+ DvrPlaybackCardPresenter(Context context) {
+ super(context);
+ mRelatedRecordingCardWidth =
+ context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width);
+ mRelatedRecordingCardHeight =
+ context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Resources res = parent.getResources();
+ RecordingCardView view = new RecordingCardView(
+ getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight);
+ return new ViewHolder(view);
+ }
+
+ @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);
+ }
+
+ @Override
+ protected String getDescription(RecordedProgram program) {
+ String description = program.getDescription();
+ if (TextUtils.isEmpty(description)) {
+ description =
+ getContext().getResources().getString(R.string.dvr_msg_no_program_description);
+ }
+ return description;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
new file mode 100644
index 00000000..1a3ae43c
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
@@ -0,0 +1,309 @@
+/*
+ * 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.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaController.TransportControls;
+import android.media.session.PlaybackState;
+import android.support.v17.leanback.app.PlaybackControlGlue;
+import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.tv.R;
+import com.android.tv.util.TimeShiftUtils;
+
+/**
+ * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and
+ * send command to the media controller. It also helps to update playback states displayed in the
+ * fragment according to information the media session provides.
+ */
+public class DvrPlaybackControlHelper extends PlaybackControlGlue {
+ private static final String TAG = "DvrPlaybackControlHelper";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Indicates the ID of the media under playback is unknown.
+ */
+ public static int UNKNOWN_MEDIA_ID = -1;
+
+ private int mPlaybackState = PlaybackState.STATE_NONE;
+ private int mPlaybackSpeedLevel;
+ private int mPlaybackSpeedId;
+ private boolean mReadyToControl;
+
+ private final MediaController mMediaController;
+ private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
+ private final TransportControls mTransportControls;
+ private final int mExtraPaddingTopForNoDescription;
+
+ public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
+ super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
+ mMediaController = activity.getMediaController();
+ mMediaController.registerCallback(mMediaControllerCallback);
+ mTransportControls = mMediaController.getTransportControls();
+ mExtraPaddingTopForNoDescription = activity.getResources()
+ .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
+ }
+
+ @Override
+ public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
+ PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+ setControlsRow(controlsRow);
+ AbstractDetailsDescriptionPresenter detailsPresenter =
+ new AbstractDetailsDescriptionPresenter() {
+ @Override
+ protected void onBindDescription(
+ AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) {
+ PlaybackControlGlue glue = (PlaybackControlGlue) object;
+ if (glue.hasValidMedia()) {
+ viewHolder.getTitle().setText(glue.getMediaTitle());
+ viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
+ } else {
+ viewHolder.getTitle().setText("");
+ viewHolder.getSubtitle().setText("");
+ }
+ if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) {
+ viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(),
+ mExtraPaddingTopForNoDescription,
+ viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom());
+ }
+ }
+ };
+ PlaybackControlsRowPresenter presenter =
+ new PlaybackControlsRowPresenter(detailsPresenter) {
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
+ super.onBindRowViewHolder(vh, item);
+ vh.setOnKeyListener(DvrPlaybackControlHelper.this);
+ }
+
+ @Override
+ protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
+ super.onUnbindRowViewHolder(vh);
+ vh.setOnKeyListener(null);
+ }
+ };
+ presenter.setProgressColor(getContext().getResources()
+ .getColor(R.color.play_controls_progress_bar_watched));
+ presenter.setBackgroundColor(getContext().getResources()
+ .getColor(R.color.play_controls_body_background_enabled));
+ presenter.setOnActionClickedListener(new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (mReadyToControl) {
+ DvrPlaybackControlHelper.super.onActionClicked(action);
+ }
+ }
+ });
+ return presenter;
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (mReadyToControl) {
+ return super.onKey(v, keyCode, event);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ PlaybackState playbackState = mMediaController.getPlaybackState();
+ if (playbackState == null) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ PlaybackState playbackState = mMediaController.getPlaybackState();
+ if (playbackState == null) {
+ return false;
+ }
+ int state = playbackState.getState();
+ return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING
+ && state != PlaybackState.STATE_PAUSED;
+ }
+
+ /**
+ * Returns the ID of the media under playback.
+ */
+ public long getMediaId() {
+ MediaMetadata mediaMetadata = mMediaController.getMetadata();
+ return mediaMetadata == null ? UNKNOWN_MEDIA_ID
+ : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ MediaMetadata mediaMetadata = mMediaController.getMetadata();
+ return mediaMetadata == null ? ""
+ : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ MediaMetadata mediaMetadata = mMediaController.getMetadata();
+ return mediaMetadata == null ? ""
+ : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE);
+ }
+
+ @Override
+ public int getMediaDuration() {
+ MediaMetadata mediaMetadata = mMediaController.getMetadata();
+ return mediaMetadata == null ? 0
+ : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ // Do not show the poster art on control row.
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return mPlaybackSpeedId;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ PlaybackState playbackState = mMediaController.getPlaybackState();
+ if (playbackState == null) {
+ return 0;
+ }
+ return (int) playbackState.getPosition();
+ }
+
+ /**
+ * Unregister media controller's callback.
+ */
+ public void unregisterCallback() {
+ mMediaController.unregisterCallback(mMediaControllerCallback);
+ }
+
+ @Override
+ protected void startPlayback(int speedId) {
+ if (getCurrentSpeedId() == speedId) {
+ return;
+ }
+ if (speedId == PLAYBACK_SPEED_NORMAL) {
+ mTransportControls.play();
+ } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) {
+ mTransportControls.rewind();
+ } else if (speedId >= PLAYBACK_SPEED_FAST_L0){
+ mTransportControls.fastForward();
+ }
+ }
+
+ @Override
+ protected void pausePlayback() {
+ mTransportControls.pause();
+ }
+
+ @Override
+ protected void skipToNext() {
+ // Do nothing.
+ }
+
+ @Override
+ protected void skipToPrevious() {
+ // Do nothing.
+ }
+
+ @Override
+ protected void onRowChanged(PlaybackControlsRow row) {
+ // Do nothing.
+ }
+
+ private void onStateChanged(int state, long positionMs, int speedLevel) {
+ if (DEBUG) Log.d(TAG, "onStateChanged");
+ getControlsRow().setCurrentTime((int) positionMs);
+ if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) {
+ // Only position is changed, no need to update controls row
+ return;
+ }
+ // NOTICE: The below two variables should only be used in this method.
+ // The only usage of them is to confirm if the state is changed or not.
+ mPlaybackState = state;
+ mPlaybackSpeedLevel = speedLevel;
+ switch (state) {
+ case PlaybackState.STATE_PLAYING:
+ mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL;
+ setFadingEnabled(true);
+ mReadyToControl = true;
+ break;
+ case PlaybackState.STATE_PAUSED:
+ mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED;
+ setFadingEnabled(true);
+ mReadyToControl = true;
+ break;
+ case PlaybackState.STATE_FAST_FORWARDING:
+ mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel;
+ setFadingEnabled(false);
+ mReadyToControl = true;
+ break;
+ case PlaybackState.STATE_REWINDING:
+ mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel;
+ setFadingEnabled(false);
+ mReadyToControl = true;
+ break;
+ case PlaybackState.STATE_CONNECTING:
+ setFadingEnabled(false);
+ mReadyToControl = false;
+ break;
+ case PlaybackState.STATE_NONE:
+ mReadyToControl = false;
+ break;
+ default:
+ setFadingEnabled(true);
+ break;
+ }
+ onStateChanged();
+ }
+
+ private class MediaControllerCallback extends MediaController.Callback {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState());
+ onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed());
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ DvrPlaybackControlHelper.this.onMetadataChanged();
+ ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
new file mode 100644
index 00000000..9184f4f7
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
@@ -0,0 +1,280 @@
+/*
+ * 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.Point;
+import android.hardware.display.DisplayManager;
+import android.media.tv.TvContentRating;
+import android.os.Bundle;
+import android.media.session.PlaybackState;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvView;
+import android.support.v17.leanback.app.PlaybackOverlayFragment;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrPlayer;
+import com.android.tv.dvr.DvrPlaybackMediaSessionHelper;
+import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.util.Utils;
+
+public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
+ // TODO: Handles audio focus. Deals with block and ratings.
+ private static final String TAG = "DvrPlaybackOverlayFragment";
+ private static final boolean DEBUG = false;
+
+ private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession";
+ private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f;
+
+ // mProgram is only used to store program from intent. Don't use it elsewhere.
+ private RecordedProgram mProgram;
+ private DvrPlaybackMediaSessionHelper mMediaSessionHelper;
+ private DvrPlaybackControlHelper mPlaybackControlHelper;
+ private ArrayObjectAdapter mRowsAdapter;
+ private ArrayObjectAdapter mRelatedRecordingsRowAdapter;
+ private DvrDataManager mDvrDataManager;
+ private ContentRatingsManager mContentRatingsManager;
+ private TvView mTvView;
+ private View mBlockScreenView;
+ private ListRow mRelatedRecordingsRow;
+ private int mExtraPaddingNoRelatedRow;
+ private int mWindowWidth;
+ private int mWindowHeight;
+ private float mAppliedAspectRatio;
+ private float mWindowAspectRatio;
+ private boolean mPinChecked;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ mExtraPaddingNoRelatedRow = getActivity().getResources()
+ .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top);
+ mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
+ mContentRatingsManager = TvApplication.getSingletons(getContext())
+ .getTvInputManagerHelper().getContentRatingsManager();
+ mProgram = getProgramFromIntent(getActivity().getIntent());
+ if (mProgram == null) {
+ Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found),
+ Toast.LENGTH_SHORT).show();
+ getActivity().finish();
+ return;
+ }
+ Point size = new Point();
+ ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
+ .getDisplay(Display.DEFAULT_DISPLAY).getSize(size);
+ mWindowWidth = size.x;
+ mWindowHeight = size.y;
+ mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight;
+ setBackgroundType(PlaybackOverlayFragment.BG_LIGHT);
+ setFadingEnabled(true);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ 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));
+ mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this);
+ setUpRows();
+ preparePlayback(getActivity().getIntent());
+ DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer();
+ dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() {
+ @Override
+ public void onAspectRatioChanged(float videoAspectRatio) {
+ updateAspectRatio(videoAspectRatio);
+ }
+ });
+ mPinChecked = getActivity().getIntent()
+ .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false);
+ dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() {
+ @Override
+ public void onContentBlocked(TvContentRating rating) {
+ if (mPinChecked) {
+ mTvView.unblockContent(rating);
+ return;
+ }
+ mBlockScreenView.setVisibility(View.VISIBLE);
+ getActivity().getMediaController().getTransportControls().pause();
+ new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR,
+ new PinDialogFragment.ResultListener() {
+ @Override
+ public void done(boolean success) {
+ if (success) {
+ mPinChecked = true;
+ mTvView.unblockContent(rating);
+ mBlockScreenView.setVisibility(View.GONE);
+ getActivity().getMediaController()
+ .getTransportControls().play();
+ }
+ }
+ }, mContentRatingsManager.getDisplayNameForRating(rating))
+ .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
+ }
+ });
+ }
+
+ @Override
+ public void onPause() {
+ if (DEBUG) Log.d(TAG, "onPause");
+ super.onPause();
+ if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING
+ || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) {
+ getActivity().getMediaController().getTransportControls().pause();
+ }
+ if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) {
+ getActivity().requestVisibleBehind(false);
+ } else {
+ getActivity().requestVisibleBehind(true);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ super.onDestroy();
+ mPlaybackControlHelper.unregisterCallback();
+ mMediaSessionHelper.release();
+ }
+
+ /**
+ * Passes the intent to the fragment.
+ */
+ public void onNewIntent(Intent intent) {
+ mProgram = getProgramFromIntent(intent);
+ if (mProgram == null) {
+ Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found),
+ Toast.LENGTH_SHORT).show();
+ // Continue playing the original program
+ return;
+ }
+ preparePlayback(intent);
+ }
+
+ /**
+ * Should be called when windows' size is changed in order to notify DVR player
+ * to update it's view width/height and position.
+ */
+ public void onWindowSizeChanged(final int windowWidth, final int windowHeight) {
+ mWindowWidth = windowWidth;
+ mWindowHeight = windowHeight;
+ mWindowAspectRatio = (float) mWindowWidth / mWindowHeight;
+ updateAspectRatio(mAppliedAspectRatio);
+ }
+
+ void onMediaControllerUpdated() {
+ mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
+ }
+
+ private void updateAspectRatio(float videoAspectRatio) {
+ if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) {
+ // No need to change
+ return;
+ }
+ if (videoAspectRatio < mWindowAspectRatio) {
+ int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2;
+ ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0);
+ } else {
+ int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2;
+ ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding);
+ }
+ mAppliedAspectRatio = videoAspectRatio;
+ }
+
+ private void preparePlayback(Intent intent) {
+ mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent));
+ getActivity().getMediaController().getTransportControls().prepare();
+ updateRelatedRecordingsRow();
+ }
+
+ private void updateRelatedRecordingsRow() {
+ boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0);
+ mRelatedRecordingsRowAdapter.clear();
+ long programId = mProgram.getId();
+ String seriesId = mProgram.getSeriesId();
+ if (!TextUtils.isEmpty(seriesId)) {
+ if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId);
+ for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) {
+ if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) {
+ mRelatedRecordingsRowAdapter.add(program);
+ }
+ }
+ }
+ View view = getView();
+ if (mRelatedRecordingsRowAdapter.size() == 0) {
+ mRowsAdapter.remove(mRelatedRecordingsRow);
+ view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow,
+ view.getPaddingRight(), view.getPaddingBottom());
+ } else if (wasEmpty){
+ mRowsAdapter.add(mRelatedRecordingsRow);
+ view.setPadding(view.getPaddingLeft(), 0,
+ view.getPaddingRight(), view.getPaddingBottom());
+ }
+ }
+
+ private void setUpRows() {
+ PlaybackControlsRowPresenter controlsRowPresenter =
+ mPlaybackControlHelper.createControlsRowAndPresenter();
+
+ ClassPresenterSelector selector = new ClassPresenterSelector();
+ selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter);
+ selector.addClassPresenter(ListRow.class, new ListRowPresenter());
+
+ mRowsAdapter = new ArrayObjectAdapter(selector);
+ mRowsAdapter.add(mPlaybackControlHelper.getControlsRow());
+ mRelatedRecordingsRow = getRelatedRecordingsRow();
+ setAdapter(mRowsAdapter);
+ }
+
+ private ListRow getRelatedRecordingsRow() {
+ mRelatedRecordingsRowAdapter =
+ new ArrayObjectAdapter(new DvrPlaybackCardPresenter(getActivity()));
+ HeaderItem header = new HeaderItem(0,
+ getActivity().getString(R.string.dvr_playback_related_recordings));
+ return new ListRow(header, mRelatedRecordingsRowAdapter);
+ }
+
+ private RecordedProgram getProgramFromIntent(Intent intent) {
+ long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1);
+ return mDvrDataManager.getRecordedProgram(programId);
+ }
+
+ private long getSeekTimeFromIntent(Intent intent) {
+ return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME,
+ TvInputManager.TIME_SHIFT_INVALID_TIME);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java b/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java
deleted file mode 100644
index 92052b5b..00000000
--- a/src/com/android/tv/dvr/ui/DvrRecordConflictFragment.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-
-import com.android.tv.data.Channel;
-import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.Program;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.guide.ProgramManager.TableEntry;
-
-import java.text.DateFormat;
-import java.util.Date;
-import java.util.List;
-
-public class DvrRecordConflictFragment extends DvrGuidedStepFragment {
- private static final int DVR_EPG_RECORD = 1;
- private static final int DVR_EPG_NOT_RECORD = 2;
-
- private List<ScheduledRecording> mConflicts;
-
- public DvrRecordConflictFragment(TableEntry entry) {
- super(entry);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mConflicts = getDvrManager().getScheduledRecordingsThatConflict(getEntry().program);
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- final MainActivity tvActivity = (MainActivity) getActivity();
- final ChannelDataManager channelDataManager = tvActivity.getChannelDataManager();
- StringBuilder sb = new StringBuilder();
- for (ScheduledRecording r : mConflicts) {
- Channel channel = channelDataManager.getChannel(r.getChannelId());
- if (channel == null) {
- continue;
- }
- sb.append(channel.getDisplayName())
- .append(" : ")
- .append(DateFormat.getDateTimeInstance().format(new Date(r.getStartTimeMs())))
- .append("\n");
- }
- String title = getResources().getString(R.string.dvr_epg_conflict_dialog_title);
- String description = sb.toString();
- 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(DVR_EPG_RECORD)
- .title(getResources().getString(R.string.dvr_epg_record))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(DVR_EPG_NOT_RECORD)
- .title(getResources().getString(R.string.dvr_epg_do_not_record))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- Program program = getEntry().program;
- if (action.getId() == DVR_EPG_RECORD) {
- getDvrManager().addSchedule(program, mConflicts);
- }
- dismissDialog();
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java b/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java
deleted file mode 100644
index d4d5cc41..00000000
--- a/src/com/android/tv/dvr/ui/DvrRecordDeleteFragment.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-
-import com.android.tv.R;
-import com.android.tv.guide.ProgramManager.TableEntry;
-
-import java.util.List;
-
-public class DvrRecordDeleteFragment extends DvrGuidedStepFragment {
- private static final int ACTION_DELETE_YES = 1;
- private static final int ACTION_DELETE_NO = 2;
-
- public DvrRecordDeleteFragment(TableEntry entry) {
- super(entry);
- }
-
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.epg_dvr_dialog_message_delete_schedule);
- return new Guidance(title, null, null, null);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_DELETE_YES)
- .title(getResources().getString(android.R.string.yes))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_DELETE_NO)
- .title(getResources().getString(android.R.string.no))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_DELETE_YES) {
- getDvrManager().removeScheduledRecording(getEntry().scheduledRecording);
- }
- dismissDialog();
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java
deleted file mode 100644
index 77e78ccc..00000000
--- a/src/com/android/tv/dvr/ui/DvrRecordScheduleFragment.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.app.FragmentManager;
-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 com.android.tv.data.Program;
-import com.android.tv.dialog.SafeDismissDialogFragment;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.guide.ProgramManager.TableEntry;
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-
-import java.util.List;
-
-public class DvrRecordScheduleFragment extends DvrGuidedStepFragment {
- private static final int ACTION_RECORD_YES = 1;
- private static final int ACTION_RECORD_NO = 2;
-
- public DvrRecordScheduleFragment(TableEntry entry) {
- super(entry);
- }
-
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.epg_dvr_dialog_message_schedule_recording);
- return new Guidance(title, null, null, null);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_RECORD_YES)
- .title(getResources().getString(android.R.string.yes))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_RECORD_NO)
- .title(getResources().getString(android.R.string.no))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- TableEntry entry = getEntry();
- Program program = entry.program;
- final List<ScheduledRecording> conflicts =
- getDvrManager().getScheduledRecordingsThatConflict(program);
- if (action.getId() == ACTION_RECORD_YES) {
- if (conflicts.isEmpty()) {
- getDvrManager().addSchedule(program, conflicts);
- dismissDialog();
- } else {
- DvrRecordConflictFragment dvrConflict = new DvrRecordConflictFragment(entry);
- SafeDismissDialogFragment currentDialog =
- ((MainActivity) getActivity()).getOverlayManager().getCurrentDialog();
- if (currentDialog instanceof DvrDialogFragment) {
- FragmentManager fm = currentDialog.getChildFragmentManager();
- GuidedStepFragment.add(fm, dvrConflict, R.id.halfsized_dialog_host);
- }
- }
- } else if (action.getId() == ACTION_RECORD_NO) {
- dismissDialog();
- }
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
new file mode 100644
index 00000000..a907b198
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -0,0 +1,140 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+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;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback;
+import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
+import com.android.tv.util.Utils;
+
+import java.util.List;
+
+/**
+ * A fragment which asks the user the type of the recording.
+ * <p>
+ * The program should be episodic and the series recording should not had been created yet.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrScheduleFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_RECORD_EPISODE = 1;
+ private static final int ACTION_RECORD_SERIES = 2;
+
+ private Program mProgram;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
+ }
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic()
+ && dvrManager.getSeriesRecording(mProgram) == null);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public int onProvideTheme() {
+ return R.style.Theme_TV_Dvr_GuidedStep_Twoline_Action;
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.dvr_schedule_dialog_title);
+ Drawable icon = getResources().getDrawable(R.drawable.ic_dvr, null);
+ return new Guidance(title, null, null, icon);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Context context = getContext();
+ String description;
+ if (mProgram.getStartTimeUtcMillis() <= System.currentTimeMillis()) {
+ description = getString(R.string.dvr_action_record_episode_from_now_description,
+ DateUtils.formatDateTime(context, mProgram.getEndTimeUtcMillis(),
+ DateUtils.FORMAT_SHOW_TIME));
+ } else {
+ description = Utils.getDurationString(context, mProgram.getStartTimeUtcMillis(),
+ mProgram.getEndTimeUtcMillis(), true);
+ }
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_RECORD_EPISODE)
+ .title(R.string.dvr_action_record_episode)
+ .description(description)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_RECORD_SERIES)
+ .title(R.string.dvr_action_record_series)
+ .description(mProgram.getTitle())
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_RECORD_EPISODE) {
+ getDvrManager().addSchedule(mProgram);
+ List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules(mProgram);
+ if (conflicts.isEmpty()) {
+ DvrUiHelper.showAddScheduleToast(getContext(), mProgram.getTitle(),
+ mProgram.getStartTimeUtcMillis(), mProgram.getEndTimeUtcMillis());
+ dismissDialog();
+ } else {
+ GuidedStepFragment fragment = new DvrProgramConflictFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, mProgram);
+ fragment.setArguments(args);
+ GuidedStepFragment.add(getFragmentManager(), fragment,
+ R.id.halfsized_dialog_host);
+ }
+ } else if (action.getId() == ACTION_RECORD_SERIES) {
+ 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();
+ }
+ });
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
new file mode 100644
index 00000000..316cb381
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.v17.leanback.app.DetailsFragment;
+
+import com.android.tv.R;
+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;
+
+/**
+ * Activity to show the list of recording schedules.
+ */
+public class DvrSchedulesActivity extends Activity {
+ /**
+ * The key for the type of the schedules which will be listed in the list. The type of the value
+ * should be {@link ScheduleListType}.
+ */
+ public static final String KEY_SCHEDULES_TYPE = "schedules_type";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE})
+ public @interface ScheduleListType {}
+ /**
+ * A type which means the activity will display the full scheduled recordings.
+ */
+ public static final int TYPE_FULL_SCHEDULE = 0;
+ /**
+ * 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;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ 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();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
new file mode 100644
index 00000000..ab695234
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.ui.SeriesDeletionFragment;
+import com.android.tv.ui.sidepanel.SettingsFragment;
+
+/**
+ * Activity to show details view in DVR.
+ */
+public class DvrSeriesDeletionActivity extends Activity {
+ /**
+ * Name of series id added to the Intent.
+ */
+ public static final String SERIES_RECORDING_ID = "series_recording_id";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_series_settings);
+ // Check savedInstanceState to prevent that activity is being showed with animation.
+ if (savedInstanceState == null) {
+ SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment();
+ deletionFragment.setArguments(getIntent().getExtras());
+ GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
new file mode 100644
index 00000000..2af78081
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+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.
+ */
+public class DvrSeriesSettingsActivity extends Activity {
+ /**
+ * Name of series id added to the Intent.
+ */
+ public static final String SERIES_RECORDING_ID = "series_recording_id";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ 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);
+ GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
new file mode 100644
index 00000000..c0e21a18
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -0,0 +1,126 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.ScheduledRecording;
+
+import java.util.List;
+
+/**
+ * A fragment which asks the user to make a recording schedule for the program.
+ * <p>
+ * If the program belongs to a series and the series recording is not created yet, we will show the
+ * option to record all the episodes of the series.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
+ private static final int ACTION_STOP = 1;
+
+ private ScheduledRecording mSchedule;
+ private DvrDataManager mDvrDataManager;
+ private final ScheduledRecordingListener mScheduledRecordingListener =
+ new ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) { }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == mSchedule.getId()) {
+ dismissDialog();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == mSchedule.getId()
+ && schedule.getState()
+ != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ dismissDialog();
+ return;
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Bundle args = getArguments();
+ long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID);
+ mSchedule = getDvrManager().getCurrentRecording(channelId);
+ if (mSchedule == null) {
+ dismissDialog();
+ return;
+ }
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
+ }
+
+ @Override
+ public void onDetach() {
+ if (mDvrDataManager != null) {
+ mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
+ }
+ super.onDetach();
+ }
+
+ @NonNull
+ @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);
+ Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
+ return new Guidance(title, description, null, image);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Context context = getContext();
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_STOP)
+ .title(R.string.dvr_action_stop)
+ .build());
+ actions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_STOP) {
+ getDvrManager().stopRecording(mSchedule);
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/EmptyItemPresenter.java b/src/com/android/tv/dvr/ui/EmptyItemPresenter.java
deleted file mode 100644
index c0305128..00000000
--- a/src/com/android/tv/dvr/ui/EmptyItemPresenter.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.content.res.Resources;
-import android.graphics.Color;
-import android.support.v17.leanback.widget.Presenter;
-import android.view.Gravity;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.data.ChannelDataManager;
-import com.android.tv.util.Utils;
-
-/**
- * Shows the item "NONE". Used for rows with now items.
- */
-public class EmptyItemPresenter extends Presenter {
-
- private final DvrBrowseFragment mMainFragment;
-
- public EmptyItemPresenter(DvrBrowseFragment mainFragment) {
- mMainFragment = mainFragment;
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
- TextView view = new TextView(parent.getContext());
- Resources resources = view.getResources();
- view.setLayoutParams(new ViewGroup.LayoutParams(
- resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width),
- resources.getDimensionPixelSize(R.dimen.dvr_card_layout_width)));
- view.setFocusable(true);
- view.setFocusableInTouchMode(true);
- view.setBackgroundColor(
- Utils.getColor(mMainFragment.getResources(), R.color.setup_background));
- view.setTextColor(Color.WHITE);
- view.setGravity(Gravity.CENTER);
- return new ViewHolder(view);
- }
-
- @Override
- public void onBindViewHolder(ViewHolder viewHolder, Object recording) {
- ((TextView) viewHolder.view).setText(
- viewHolder.view.getContext().getString(R.string.dvr_msg_no_recording_on_the_row));
- }
-
- @Override
- public void onUnbindViewHolder(ViewHolder viewHolder) { }
-}
diff --git a/src/com/android/tv/dvr/ui/EmptyHolder.java b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java
index 45cd3a36..d4d4d8ab 100644
--- a/src/com/android/tv/dvr/ui/EmptyHolder.java
+++ b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java
@@ -17,11 +17,13 @@
package com.android.tv.dvr.ui;
/**
- * Special object meaning a row is empty;
+ * Special object for schedule preview;
*/
-final class EmptyHolder {
- static final EmptyHolder EMPTY_HOLDER = new EmptyHolder();
+final class FullScheduleCardHolder {
+ /**
+ * Full schedule card holder.
+ */
+ static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder();
- private EmptyHolder() {
- }
+ private FullScheduleCardHolder() { }
}
diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java
new file mode 100644
index 00000000..7dd85f45
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java
@@ -0,0 +1,84 @@
+/*
+ * 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.support.v17.leanback.widget.Presenter;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
+ */
+public class FullSchedulesCardPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new ScheduledRecordingViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ final Context context = viewHolder.view.getContext();
+
+ cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule));
+ cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title));
+ List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context)
+ .getDvrDataManager().getAvailableScheduledRecordings();
+ int fullDays = 0;
+ if (!scheduledRecordings.isEmpty()) {
+ fullDays = Utils.computeDateDifference(System.currentTimeMillis(),
+ Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR)
+ .getStartTimeMs()) + 1;
+ }
+ cardView.setContent(context.getResources().getQuantityString(
+ R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null);
+
+ View.OnClickListener clickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ DvrUiHelper.startSchedulesActivity(context, null);
+ }
+ };
+ baseHolder.view.setOnClickListener(clickListener);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder baseHolder) {
+ ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ cardView.reset();
+ }
+
+ private static final class ScheduledRecordingViewHolder extends ViewHolder {
+ ScheduledRecordingViewHolder(RecordingCardView view) {
+ super(view);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
index dc89a8e0..fcf0925b 100644
--- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
@@ -1,21 +1,71 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.tv.dvr.ui;
+import android.content.DialogInterface;
import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.R;
+import com.android.tv.dialog.SafeDismissDialogFragment;
+
+import java.util.concurrent.TimeUnit;
public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName();
public static final String TRACKER_LABEL = "Half sized dialog";
+ private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30);
+
+ private Handler mHandler = new Handler();
+ private Runnable mAutoDismisser = new Runnable() {
+ @Override
+ public void run() {
+ dismiss();
+ }
+ };
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- return inflater.inflate(R.layout.halfsized_dialog, null);
+ return inflater.inflate(R.layout.halfsized_dialog, container, false);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) {
+ mHandler.removeCallbacks(mAutoDismisser);
+ mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
+ return false;
+ }
+ });
+ mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mHandler.removeCallbacks(mAutoDismisser);
}
@Override
@@ -27,4 +77,4 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
public String getTrackerLabel() {
return TRACKER_LABEL;
}
-}
+} \ 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
new file mode 100644
index 00000000..9f78985f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
@@ -0,0 +1,252 @@
+/*
+ * 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.FragmentManager;
+import android.content.Context;
+import android.graphics.Typeface;
+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.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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.DvrScheduleManager;
+import com.android.tv.dvr.SeriesRecording;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fragment for DVR series recording settings.
+ */
+public class PrioritySettingsFragment extends GuidedStepFragment {
+ /**
+ * Name of series recording id starting the fragment.
+ * Type: Long
+ */
+ public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id";
+
+ private static final int ONE_TIME_RECORDING_ID = 0;
+ // 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;
+ private SeriesRecording mComeFromSeriesRecording;
+ private float mSelectedActionElevation;
+ private int mActionColor;
+ private int mSelectedActionColor;
+
+ @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());
+ long comeFromSeriesRecordingId =
+ getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1);
+ for (SeriesRecording series : mDvrDataManager.getSeriesRecordings()) {
+ if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL
+ || series.getId() == comeFromSeriesRecordingId) {
+ mSeriesRecordings.add(series);
+ }
+ }
+ mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR);
+ mComeFromSeriesRecording = mDvrDataManager.getSeriesRecording(comeFromSeriesRecordingId);
+ mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal);
+ mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null);
+ mSelectedActionColor =
+ getResources().getColor(R.color.dvr_guided_step_action_text_color_selected, null);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setSelectedActionPosition(mComeFromSeriesRecording == null ? 1
+ : mSeriesRecordings.indexOf(mComeFromSeriesRecording));
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String breadcrumb = mComeFromSeriesRecording == null ? null
+ : mComeFromSeriesRecording.getTitle();
+ return new Guidance(getString(R.string.dvr_priority_title),
+ getString(R.string.dvr_priority_description), breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ int position = 0;
+ for (SeriesRecording seriesRecording : mSeriesRecordings) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(position++)
+ .title(seriesRecording.getTitle())
+ .build());
+ }
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_SAVE)
+ .title(getString(R.string.dvr_priority_button_action_save))
+ .build());
+ actions.add(new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_ID_SAVE) {
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ int size = mSeriesRecordings.size();
+ for (int i = 1; i < size; ++i) {
+ long priority = DvrScheduleManager.suggestSeriesPriority(size - i);
+ SeriesRecording seriesRecording = mSeriesRecordings.get(i);
+ if (seriesRecording.getPriority() != priority) {
+ dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording)
+ .setPriority(priority).build());
+ }
+ }
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.popBackStack();
+ } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.popBackStack();
+ } else if (mSelectedRecording == null) {
+ mSelectedRecording = mSeriesRecordings.get((int) actionId);
+ for (int i = 0; i < mSeriesRecordings.size(); ++i) {
+ updateItem(i);
+ }
+ } else {
+ mSelectedRecording = null;
+ for (int i = 0; i < mSeriesRecordings.size(); ++i) {
+ updateItem(i);
+ }
+ }
+ }
+
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ super.onGuidedActionFocused(action);
+ if (mSelectedRecording == null) {
+ return;
+ }
+ if (action.getId() < 0) {
+ int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording);
+ mSelectedRecording = null;
+ for (int i = 0; i < mSeriesRecordings.size(); ++i) {
+ updateItem(i);
+ }
+ return;
+ }
+ int position = (int) action.getId();
+ int previousPosition = mSeriesRecordings.indexOf(mSelectedRecording);
+ mSeriesRecordings.remove(mSelectedRecording);
+ mSeriesRecordings.add(position, mSelectedRecording);
+ updateItem(previousPosition);
+ updateItem(position);
+ notifyActionChanged(previousPosition);
+ notifyActionChanged(position);
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ return new DvrGuidedActionsStylist(true);
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new DvrGuidedActionsStylist(false) {
+ @Override
+ public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
+ super.onBindViewHolder(vh, action);
+ updateItem(vh.itemView, (int) action.getId());
+ }
+
+ @Override
+ public int onProvideItemLayoutId() {
+ return R.layout.priority_settings_action_item;
+ }
+ };
+ }
+
+ private void updateItem(int position) {
+ View itemView = getActionItemView(position);
+ if (itemView == null) {
+ return;
+ }
+ updateItem(itemView, position);
+ }
+
+ private void updateItem(View itemView, int position) {
+ GuidedAction action = getActions().get(position);
+ action.setTitle(mSeriesRecordings.get(position).getTitle());
+ boolean selected = mSelectedRecording != null
+ && mSeriesRecordings.indexOf(mSelectedRecording) == position;
+ TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title);
+ ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image);
+ if (position == 0) {
+ // one-time recording
+ itemView.setBackgroundResource(R.drawable.setup_selector_background);
+ imageView.setVisibility(View.GONE);
+ itemView.setFocusable(false);
+ itemView.setElevation(0);
+ // strings.xml <i> tag doesn't work.
+ titleView.setTypeface(titleView.getTypeface(), Typeface.ITALIC);
+ } else if (mSelectedRecording == null) {
+ titleView.setTextColor(mActionColor);
+ itemView.setBackgroundResource(R.drawable.setup_selector_background);
+ imageView.setImageResource(R.drawable.ic_draggable_white);
+ imageView.setVisibility(View.VISIBLE);
+ itemView.setFocusable(true);
+ itemView.setElevation(0);
+ titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL);
+ } else if (selected) {
+ titleView.setTextColor(mSelectedActionColor);
+ itemView.setBackgroundResource(R.drawable.priority_settings_action_item_selected);
+ imageView.setImageResource(R.drawable.ic_dragging_grey);
+ imageView.setVisibility(View.VISIBLE);
+ itemView.setFocusable(true);
+ itemView.setElevation(mSelectedActionElevation);
+ titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL);
+ } else {
+ titleView.setTextColor(mActionColor);
+ itemView.setBackgroundResource(R.drawable.setup_selector_background);
+ imageView.setVisibility(View.INVISIBLE);
+ itemView.setFocusable(true);
+ itemView.setElevation(0);
+ titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
new file mode 100644
index 00000000..9eb7e385
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
@@ -0,0 +1,218 @@
+/*
+ * 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.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;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+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.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 {
+ 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;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
+ .getDvrWatchedPositionManager();
+ mTvInputManagerHelper = TvApplication.getSingletons(getActivity())
+ .getTvInputManagerHelper();
+ setDetailsOverviewRow(mDetailsContent);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateActions();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ mRecordedProgram = TvApplication.getSingletons(getActivity()).getDvrDataManager()
+ .getRecordedProgram(recordedProgramId);
+ if (mRecordedProgram == null) {
+ // notify super class to end activity before initializing anything
+ return false;
+ }
+ mDetailsContent = createDetailsContent();
+ return true;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mRecordedProgram.getChannelId());
+ String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription())
+ ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription();
+ return new DetailsContent.Builder()
+ .setTitle(getTitleFromProgram(mRecordedProgram, channel))
+ .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis())
+ .setDescription(description)
+ .setImageUris(mRecordedProgram, channel)
+ .build();
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (mDvrWatchedPositionManager.getWatchedPosition(mRecordedProgram.getId())
+ != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ 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)));
+ adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_play_from_beginning), null,
+ res.getDrawable(R.drawable.lb_ic_replay)));
+ } else {
+ adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_watch), null,
+ res.getDrawable(R.drawable.lb_ic_play)));
+ }
+ adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING,
+ res.getString(R.string.dvr_detail_delete), null,
+ res.getDrawable(R.drawable.ic_delete_32dp)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
+ startPlayback(TvInputManager.TIME_SHIFT_INVALID_TIME);
+ } else if (action.getId() == ACTION_RESUME_PLAYING) {
+ startPlayback(mDvrWatchedPositionManager
+ .getWatchedPosition(mRecordedProgram.getId()));
+ } else if (action.getId() == ACTION_DELETE_RECORDING) {
+ DvrManager dvrManager = TvApplication
+ .getSingletons(getActivity()).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordedProgram);
+ getActivity().finish();
+ }
+ }
+ };
+ }
+
+ 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 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);
+ }
+ }
+
+ 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);
+ }
+ 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 0b656bdc..704d3a3f 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
+++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
@@ -17,105 +17,180 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
-import android.app.AlertDialog;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.Resources;
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 java.util.List;
-
-import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.recording.RecordedProgram;
+import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.dvr.DvrManager;
-import com.android.tv.ui.DialogUtils;
+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;
+import java.util.concurrent.TimeUnit;
+
/**
* Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}.
*/
-public class RecordedProgramPresenter extends Presenter {
+public class RecordedProgramPresenter extends Presenter implements OnClickListener {
private final ChannelDataManager mChannelDataManager;
+ private final DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private final Context mContext;
+ private String mTodayString;
+ private String mYesterdayString;
+ private final int mProgressBarColor;
+ private final boolean mShowEpisodeTitle;
- public RecordedProgramPresenter(Context context) {
+ private static final class RecordedProgramViewHolder extends ViewHolder
+ implements WatchedPositionChangedListener {
+ private RecordedProgram mProgram;
+
+ RecordedProgramViewHolder(RecordingCardView view, int progressColor) {
+ super(view);
+ view.setProgressBarColor(progressColor);
+ }
+
+ private void setProgram(RecordedProgram program) {
+ mProgram = program;
+ }
+
+ private void setProgressBar(long watchedPositionMs) {
+ ((RecordingCardView) view).setProgressBar(
+ (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null
+ : Math.min(100, (int) (100.0f * watchedPositionMs
+ / mProgram.getDurationMillis())));
+ }
+
+ @Override
+ public void onWatchedPositionChanged(long programId, long positionMs) {
+ if (programId == mProgram.getId()) {
+ setProgressBar(positionMs);
+ }
+ }
+ }
+
+ public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) {
+ mContext = context;
mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
+ mTodayString = context.getString(R.string.dvr_date_today);
+ mYesterdayString = context.getString(R.string.dvr_date_yesterday);
+ mDvrWatchedPositionManager =
+ TvApplication.getSingletons(context).getDvrWatchedPositionManager();
+ mProgressBarColor = context.getResources()
+ .getColor(R.color.play_controls_progress_bar_watched);
+ mShowEpisodeTitle = showEpisodeTitle;
+ }
+
+ public RecordedProgramPresenter(Context context) {
+ this(context, false);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
- Context context = parent.getContext();
- RecordingCardView view = new RecordingCardView(context);
- return new ViewHolder(view);
+ RecordingCardView view = new RecordingCardView(mContext);
+ return new RecordedProgramViewHolder(view, mProgressBarColor);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object o) {
- final RecordedProgram recording = (RecordedProgram) o;
+ final RecordedProgram program = (RecordedProgram) o;
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- final Context context = viewHolder.view.getContext();
- final Resources resources = context.getResources();
-
- Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
-
- if (!TextUtils.isEmpty(recording.getTitle())) {
- cardView.setTitle(recording.getTitle());
+ cardView.setTag(program);
+ Channel channel = mChannelDataManager.getChannel(program.getChannelId());
+ SpannableString title;
+ if (mShowEpisodeTitle) {
+ title = new SpannableString(program.getEpisodeDisplayTitle(mContext));
} else {
- cardView.setTitle(resources.getString(R.string.dvr_msg_program_title_unknown));
+ String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(mContext);
+ title = titleWithEpisodeNumber == null ? null
+ : new SpannableString(titleWithEpisodeNumber);
}
- if (recording.getPosterArt() != null) {
- cardView.setImageUri(recording.getPosterArt());
- } else if (recording.getThumbnail() != null) {
- cardView.setImageUri(recording.getThumbnail());
- } else {
- if (channel != null) {
- cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString());
- }
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : mContext.getResources().getString(R.string.no_program_information));
+ } else if (!mShowEpisodeTitle) {
+ String programTitle = program.getTitle();
+ title.setSpan(new TextAppearanceSpan(mContext,
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ cardView.setTitle(title);
+ String imageUri = null;
+ boolean isChannelLogo = false;
+ if (program.getPosterArtUri() != null) {
+ imageUri = program.getPosterArtUri();
+ } else if (program.getThumbnailUri() != null) {
+ imageUri = program.getThumbnailUri();
+ } else if (channel != null) {
+ imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString();
+ isChannelLogo = true;
+ }
+ cardView.setImageUri(imageUri, isChannelLogo);
+ int durationMinutes =
+ Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis()));
+ 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);
+ mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId());
+ cardViewHolder
+ .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId()));
}
- cardView.setContent(Utils.getDurationString(context, recording.getStartTimeUtcMillis(),
- recording.getEndTimeUtcMillis(), true));
- //TODO: replace with a detail card
- viewHolder.view.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- DialogUtils.showListDialog(v.getContext(),
- new int[] { R.string.dvr_detail_play, R.string.dvr_detail_delete },
- new Runnable[] {
- new Runnable() {
- @Override
- public void run() {
- Intent intent = new Intent(context, MainActivity.class);
- intent.putExtra(Utils.EXTRA_KEY_RECORDING_URI,
- recording.getUri());
- context.startActivity(intent);
- ((Activity) context).finish();
- }
- },
- new Runnable() {
- @Override
- public void run() {
- DvrManager dvrManager = TvApplication
- .getSingletons(context).getDvrManager();
- dvrManager.removeRecordedProgram(recording);
- }
- },
- });
- }
- });
-
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- cardView.reset();
+ if (viewHolder instanceof RecordedProgramViewHolder) {
+ mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder,
+ ((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());
+ }
+ }
+
+ /**
+ * Returns description would be used in its card view.
+ */
+ protected String getDescription(RecordedProgram recording) {
+ int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(),
+ System.currentTimeMillis());
+ if (dateDifference == 0) {
+ return mTodayString;
+ } else if (dateDifference == 1) {
+ return mYesterdayString;
+ } else {
+ return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(),
+ recording.getStartTimeUtcMillis(), false, true, false, 0);
+ }
+ }
+
+ /**
+ * Returns context.
+ */
+ protected Context getContext() {
+ return mContext;
}
}
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java b/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.java
deleted file mode 100644
index eeb26041..00000000
--- a/src/com/android/tv/dvr/ui/RecordedProgramsAdapter.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.support.v17.leanback.widget.PresenterSelector;
-
-import com.android.tv.common.recording.RecordedProgram;
-import com.android.tv.dvr.DvrDataManager;
-
-/**
- * Adapter for {@link RecordedProgram}.
- */
-final class RecordedProgramsAdapter extends SortedArrayAdapter<RecordedProgram>
- implements DvrDataManager.RecordedProgramListener {
- private final DvrDataManager mDataManager;
-
- RecordedProgramsAdapter(DvrDataManager dataManager, PresenterSelector presenterSelector) {
- super(presenterSelector, RecordedProgram.START_TIME_THEN_ID_COMPARATOR);
- mDataManager = dataManager;
- }
-
- public void start() {
- clear();
- addAll(mDataManager.getRecordedPrograms());
- mDataManager.addRecordedProgramListener(this);
- }
-
- public void stop() {
- mDataManager.removeRecordedProgramListener(this);
- }
-
- @Override
- long getId(RecordedProgram item) {
- return item.getId();
- }
-
- @Override // DvrDataManager.RecordedProgramListener
- public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
- add(recordedProgram);
- }
-
- @Override // DvrDataManager.RecordedProgramListener
- public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
- change(recordedProgram);
- }
-
- @Override // DvrDataManager.RecordedProgramListener
- public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
- remove(recordedProgram);
- }
-}
diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java
index def11248..fa4562fd 100644
--- a/src/com/android/tv/dvr/ui/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/RecordingCardView.java
@@ -25,15 +25,19 @@ import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.BaseCardView;
import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.View;
import android.widget.ImageView;
+import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.tv.R;
+import com.android.tv.dvr.RecordedProgram;
import com.android.tv.util.ImageLoader;
/**
* A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or
- * {@link com.android.tv.common.recording.RecordedProgram}
+ * {@link RecordedProgram} or
+ * {@link com.android.tv.dvr.SeriesRecording}.
*/
class RecordingCardView extends BaseCardView {
private final ImageView mImageView;
@@ -41,36 +45,81 @@ class RecordingCardView extends BaseCardView {
private final int mImageHeight;
private String mImageUri;
private final TextView mTitleView;
- private final TextView mContentView;
+ private final TextView mMajorContentView;
+ private final TextView mMinorContentView;
+ private final ProgressBar mProgressBar;
private final Drawable mDefaultImage;
RecordingCardView(Context context) {
+ this(context,
+ context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width),
+ context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height));
+ }
+
+ RecordingCardView(Context context, int imageWidth, int imageHeight) {
super(context);
//TODO(dvr): move these to the layout XML.
setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
+ setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS);
setFocusable(true);
setFocusableInTouchMode(true);
- mDefaultImage = getResources().getDrawable(R.drawable.default_now_card, null);
+ mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null);
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.dvr_recording_card_view, this);
-
mImageView = (ImageView) findViewById(R.id.image);
- mImageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width);
- mImageHeight = getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width);
+ mImageWidth = imageWidth;
+ mImageHeight = imageHeight;
+ mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
mTitleView = (TextView) findViewById(R.id.title);
- mContentView = (TextView) findViewById(R.id.content);
+ mMajorContentView = (TextView) findViewById(R.id.content_major);
+ mMinorContentView = (TextView) findViewById(R.id.content_minor);
}
void setTitle(CharSequence title) {
mTitleView.setText(title);
}
- void setContent(CharSequence content) {
- mContentView.setText(content);
+ void setContent(CharSequence majorContent, CharSequence minorContent) {
+ if (!TextUtils.isEmpty(majorContent)) {
+ mMajorContentView.setText(majorContent);
+ mMajorContentView.setVisibility(View.VISIBLE);
+ } else {
+ mMajorContentView.setVisibility(View.GONE);
+ }
+ if (!TextUtils.isEmpty(minorContent)) {
+ mMinorContentView.setText(minorContent);
+ mMinorContentView.setVisibility(View.VISIBLE);
+ } else {
+ mMinorContentView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Sets progress bar. If progress is {@code null}, hides progress bar.
+ */
+ void setProgressBar(Integer progress) {
+ if (progress == null) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setProgress(progress);
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
}
- void setImageUri(String uri) {
+ /**
+ * Sets the color of progress bar.
+ */
+ void setProgressBarColor(int color) {
+ mProgressBar.getProgressDrawable().setTint(color);
+ }
+
+ void setImageUri(String uri, boolean isChannelLogo) {
+ if (isChannelLogo) {
+ mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ } else {
+ mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ }
mImageUri = uri;
if (TextUtils.isEmpty(uri)) {
mImageView.setImageDrawable(mDefaultImage);
@@ -80,14 +129,22 @@ class RecordingCardView extends BaseCardView {
}
}
- public void setImageUri(Uri uri) {
- if (uri != null) {
- setImageUri(uri.toString());
- } else {
- setImageUri("");
+ /**
+ * Set image to card view.
+ */
+ public void setImage(Drawable image) {
+ if (image != null) {
+ mImageView.setImageDrawable(image);
}
}
+ /**
+ * Returns image view.
+ */
+ public ImageView getImageView() {
+ return mImageView;
+ }
+
private static class RecordingCardImageLoaderCallback
extends ImageLoader.ImageLoaderCallback<RecordingCardView> {
private final String mUri;
@@ -108,8 +165,8 @@ class RecordingCardView extends BaseCardView {
}
public void reset() {
- mTitleView.setText("");
- mContentView.setText("");
+ mTitleView.setText(null);
+ setContent(null, null);
mImageView.setImageDrawable(mDefaultImage);
}
}
diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
new file mode 100644
index 00000000..2271d932
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
@@ -0,0 +1,94 @@
+/*
+ * 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.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.ScheduledRecording;
+
+/**
+ * {@link DetailsFragment} for recordings in DVR.
+ */
+abstract class RecordingDetailsFragment extends DvrDetailsFragment {
+ private ScheduledRecording mRecording;
+ private DetailsContent mDetailsContent;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsContent = createDetailsContent();
+ setDetailsOverviewRow(mDetailsContent);
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ 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;
+ }
+
+ /**
+ * Returns {@link ScheduledRecording} for the current fragment.
+ */
+ public ScheduledRecording getRecording() {
+ return mRecording;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mRecording.getChannelId());
+ SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ?
+ null : new SpannableString(mRecording
+ .getProgramTitleWithEpisodeNumber(getContext()));
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : getContext().getResources().getString(
+ R.string.no_program_information));
+ } else {
+ String programTitle = mRecording.getProgramTitle();
+ title.setSpan(new TextAppearanceSpan(getContext(),
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ?
+ mRecording.getProgramDescription() : mRecording.getProgramLongDescription();
+ if (TextUtils.isEmpty(description)) {
+ description = channel != null ? channel.getDescription() : null;
+ }
+ return new DetailsContent.Builder()
+ .setTitle(title)
+ .setStartTimeUtcMillis(mRecording.getStartTimeMs())
+ .setEndTimeUtcMillis(mRecording.getEndTimeMs())
+ .setDescription(description)
+ .setImageUris(mRecording.getProgramPosterArtUri(),
+ mRecording.getProgramThumbnailUri(), channel)
+ .build();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
new file mode 100644
index 00000000..5c1ba48c
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
+
+/**
+ * {@link RecordingDetailsFragment} for scheduled recording in DVR.
+ */
+public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment {
+ private static final int ACTION_VIEW_SCHEDULE = 1;
+ private static final int ACTION_CANCEL = 2;
+
+ private DvrManager mDvrManager;
+ private Action mScheduleAction;
+ private boolean mHideViewSchedule;
+
+ @Override
+ public void onCreate(Bundle savedInstance) {
+ mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
+ super.onCreate(savedInstance);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mScheduleAction != null) {
+ mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId()));
+ }
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (!mHideViewSchedule) {
+ mScheduleAction = new Action(ACTION_VIEW_SCHEDULE,
+ res.getString(R.string.dvr_detail_view_schedule), null,
+ res.getDrawable(getScheduleIconId()));
+ adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction);
+ }
+ adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL,
+ res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null,
+ res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_SCHEDULE) {
+ DvrUiHelper.startSchedulesActivity(getContext(), getRecording());
+ } else if (actionId == ACTION_CANCEL) {
+ mDvrManager.removeScheduledRecording(getRecording());
+ getActivity().finish();
+ }
+ }
+ };
+ }
+
+ private int getScheduleIconId() {
+ if (mDvrManager.isConflicting(getRecording())) {
+ return R.drawable.ic_warning_white_36dp;
+ } 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 533a4882..1f67bbe3 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
@@ -16,156 +16,165 @@
package com.android.tv.dvr.ui;
-import android.app.AlertDialog;
+import android.app.Activity;
import android.content.Context;
-import android.content.DialogInterface;
import android.media.tv.TvContract;
-import android.support.annotation.Nullable;
+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 android.widget.Toast;
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.data.Program;
-import com.android.tv.data.ProgramDataManager;
-import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.util.Utils;
+import java.util.concurrent.TimeUnit;
+
/**
* Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
*/
public class ScheduledRecordingPresenter extends Presenter {
+ private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
+
private final ChannelDataManager mChannelDataManager;
+ private final Context mContext;
+ private final int mProgressBarColor;
private static final class ScheduledRecordingViewHolder extends ViewHolder {
- private ProgramDataManager.QueryProgramTask mQueryProgramTask;
+ private final Handler mHandler = new Handler();
+ private ScheduledRecording mScheduledRecording;
+ private final Runnable mProgressBarUpdater = new Runnable() {
+ @Override
+ public void run() {
+ updateProgressBar();
+ mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS);
+ }
+ };
- ScheduledRecordingViewHolder(RecordingCardView view) {
+ ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) {
super(view);
+ view.setProgressBarColor(progressBarColor);
+ }
+
+ private void updateProgressBar() {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ int recordingState = mScheduledRecording.getState();
+ RecordingCardView cardView = (RecordingCardView) view;
+ if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ cardView.setProgressBar(Math.max(0, Math.min((int) (100 *
+ (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs())
+ / mScheduledRecording.getDuration()), 100)));
+ } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) {
+ cardView.setProgressBar(100);
+ } else {
+ // Hides progress bar.
+ cardView.setProgressBar(null);
+ }
+ }
+
+ private void startUpdateProgressBar() {
+ mHandler.post(mProgressBarUpdater);
+ }
+
+ private void stopUpdateProgressBar() {
+ mHandler.removeCallbacks(mProgressBarUpdater);
}
}
public ScheduledRecordingPresenter(Context context) {
ApplicationSingletons singletons = TvApplication.getSingletons(context);
mChannelDataManager = singletons.getChannelDataManager();
+ mContext = context;
+ mProgressBarColor = context.getResources()
+ .getColor(R.color.play_controls_recording_icon_color_on_focus);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Context context = parent.getContext();
RecordingCardView view = new RecordingCardView(context);
- return new ScheduledRecordingViewHolder(view);
+ return new ScheduledRecordingViewHolder(view, mProgressBarColor);
}
@Override
public void onBindViewHolder(ViewHolder baseHolder, Object o) {
- ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
final ScheduledRecording recording = (ScheduledRecording) o;
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
final Context context = viewHolder.view.getContext();
- long programId = recording.getProgramId();
- if (programId == ScheduledRecording.ID_NOT_SET) {
- setTitleAndImage(cardView, recording, null);
+ setTitleAndImage(cardView, recording);
+ int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(),
+ recording.getStartTimeMs());
+ if (dateDifference <= 0) {
+ cardView.setContent(mContext.getString(R.string.dvr_date_today_time,
+ Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0)), null);
+ } else if (dateDifference == 1) {
+ cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time,
+ Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0)), null);
} else {
- viewHolder.mQueryProgramTask = new ProgramDataManager.QueryProgramTask(
- context.getContentResolver(), programId) {
- @Override
- protected void onPostExecute(Program program) {
- super.onPostExecute(program);
- setTitleAndImage(cardView, recording, program);
- }
- };
- viewHolder.mQueryProgramTask.executeOnDbThread();
-
+ cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getStartTimeMs(), false, true, false, 0), null);
}
- cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(),
- recording.getEndTimeMs(), true));
- //TODO: replace with a detail card
+ viewHolder.updateProgressBar();
View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
- switch (recording.getState()) {
- case ScheduledRecording.STATE_RECORDING_NOT_STARTED: {
- showScheduledRecordingDialog(v.getContext(), recording);
- break;
- }
- case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: {
- showCurrentlyRecordingDialog(v.getContext(), recording);
- break;
- }
+ if (v instanceof RecordingCardView) {
+ DvrUiHelper.startDetailsActivity((Activity) v.getContext(), recording,
+ ((RecordingCardView) v).getImageView(), false);
}
}
};
baseHolder.view.setOnClickListener(clickListener);
- }
-
- private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording,
- @Nullable Program program) {
- if (program != null) {
- cardView.setTitle(program.getTitle());
- cardView.setImageUri(program.getPosterArtUri());
- } else {
- cardView.setTitle(
- cardView.getResources().getString(R.string.dvr_msg_program_title_unknown));
- Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
- if (channel != null) {
- cardView.setImageUri(TvContract.buildChannelLogoUri(channel.getId()).toString());
- }
- }
+ viewHolder.mScheduledRecording = recording;
+ viewHolder.startUpdateProgressBar();
}
@Override
public void onUnbindViewHolder(ViewHolder baseHolder) {
ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ viewHolder.stopUpdateProgressBar();
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- if (viewHolder.mQueryProgramTask != null) {
- viewHolder.mQueryProgramTask.cancel(true);
- viewHolder.mQueryProgramTask = null;
- }
+ viewHolder.mScheduledRecording = null;
cardView.reset();
}
- private void showScheduledRecordingDialog(final Context context,
- final ScheduledRecording recording) {
- DialogInterface.OnClickListener removeScheduleListener
- = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- // TODO(DVR) handle success/failure.
- DvrManager dvrManager = TvApplication.getSingletons(context)
- .getDvrManager();
- dvrManager.removeScheduledRecording((ScheduledRecording) recording);
- }
- };
- new AlertDialog.Builder(context)
- .setMessage(R.string.epg_dvr_dialog_message_remove_recording_schedule)
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes, removeScheduleListener)
- .show();
- }
-
- private void showCurrentlyRecordingDialog(final Context context,
- final ScheduledRecording recording) {
- DialogInterface.OnClickListener stopRecordingListener
- = new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- DvrManager dvrManager = TvApplication.getSingletons(context)
- .getDvrManager();
- dvrManager.stopRecording((ScheduledRecording) recording);
- }
- };
- new AlertDialog.Builder(context)
- .setMessage(R.string.epg_dvr_dialog_message_stop_recording)
- .setNegativeButton(android.R.string.no, null)
- .setPositiveButton(android.R.string.yes, stopRecordingListener)
- .show();
+ private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) {
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+ SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ?
+ null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext));
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : mContext.getResources().getString(R.string.no_program_information));
+ } else {
+ String programTitle = recording.getProgramTitle();
+ title.setSpan(new TextAppearanceSpan(mContext,
+ R.style.text_appearance_card_view_episode_number),
+ programTitle == null ? 0 : programTitle.length(), title.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ String imageUri = recording.getProgramPosterArtUri();
+ boolean isChannelLogo = false;
+ if (TextUtils.isEmpty(imageUri)) {
+ imageUri = channel != null ?
+ TvContract.buildChannelLogoUri(channel.getId()).toString() : null;
+ isChannelLogo = true;
+ }
+ cardView.setTitle(title);
+ cardView.setImageUri(imageUri, isChannelLogo);
}
}
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java
deleted file mode 100644
index 65955276..00000000
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingsAdapter.java
+++ /dev/null
@@ -1,85 +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.support.v17.leanback.widget.PresenterSelector;
-
-import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.ScheduledRecording;
-
-/**
- * Adapter for {@link ScheduledRecording} filtered by
- * {@link com.android.tv.dvr.ScheduledRecording.RecordingState}.
- */
-final class ScheduledRecordingsAdapter extends SortedArrayAdapter<ScheduledRecording>
- implements DvrDataManager.ScheduledRecordingListener {
- private final int mState;
- private final DvrDataManager mDataManager;
-
- ScheduledRecordingsAdapter(DvrDataManager dataManager, int state,
- PresenterSelector presenterSelector) {
- super(presenterSelector, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
- mDataManager = dataManager;
- mState = state;
- }
-
- public void start() {
- clear();
- switch (mState) {
- case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
- addAll(mDataManager.getNonStartedScheduledRecordings());
- break;
- case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
- addAll(mDataManager.getStartedRecordings());
- break;
- default:
- throw new IllegalStateException("Unknown recording state " + mState);
-
- }
- mDataManager.addScheduledRecordingListener(this);
- }
-
- public void stop() {
- mDataManager.removeScheduledRecordingListener(this);
- }
-
- @Override
- long getId(ScheduledRecording item) {
- return item.getId();
- }
-
- @Override //DvrDataManager.ScheduledRecordingListener
- public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) {
- if (scheduledRecording.getState() == mState) {
- add(scheduledRecording);
- }
- }
-
- @Override //DvrDataManager.ScheduledRecordingListener
- public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) {
- remove(scheduledRecording);
- }
-
- @Override //DvrDataManager.ScheduledRecordingListener
- public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) {
- if (scheduledRecording.getState() == mState) {
- change(scheduledRecording);
- } else {
- remove(scheduledRecording);
- }
- }
-}
diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
new file mode 100644
index 00000000..c29d62ae
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
@@ -0,0 +1,249 @@
+/*
+ * 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.media.tv.TvInputManager;
+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.text.TextUtils;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.ui.GuidedActionsStylistWithDivider;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Fragment for DVR series recording settings.
+ */
+public class SeriesDeletionFragment extends GuidedStepFragment {
+ private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
+
+ // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers,
+ // negative values are used by other actions to prevent duplicated IDs.
+ private static final long ACTION_ID_SELECT_WATCHED = -110;
+ private static final long ACTION_ID_SELECT_ALL = -111;
+ private static final long ACTION_ID_DELETE = -112;
+
+ private DvrDataManager mDvrDataManager;
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private List<RecordedProgram> mRecordings;
+ private final Set<Long> mWatchedRecordings = new HashSet<>();
+ private boolean mAllSelected;
+ private long mSeriesRecordingId;
+ private int mOneLineActionHeight;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mSeriesRecordingId = getArguments()
+ .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
+ SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrWatchedPositionManager =
+ TvApplication.getSingletons(context).getDvrWatchedPositionManager();
+ mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
+ mOneLineActionHeight = getResources().getDimensionPixelSize(
+ R.dimen.dvr_settings_one_line_action_container_height);
+ if (mRecordings.isEmpty()) {
+ Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings),
+ Toast.LENGTH_LONG).show();
+ finishGuidedStepFragments();
+ return;
+ }
+ Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String breadcrumb = null;
+ SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
+ if (series != null) {
+ breadcrumb = series.getTitle();
+ }
+ return new Guidance(getString(R.string.dvr_series_deletion_title),
+ getString(R.string.dvr_series_deletion_description), breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_SELECT_WATCHED)
+ .title(getString(R.string.dvr_series_select_watched))
+ .build());
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_SELECT_ALL)
+ .title(getString(R.string.dvr_series_select_all))
+ .build());
+ actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext()));
+ for (RecordedProgram recording : mRecordings) {
+ long watchedPositionMs =
+ mDvrWatchedPositionManager.getWatchedPosition(recording.getId());
+ String title = recording.getEpisodeDisplayTitle(getContext());
+ if (TextUtils.isEmpty(title)) {
+ title = TextUtils.isEmpty(recording.getTitle()) ?
+ getString(R.string.channel_banner_no_title) : recording.getTitle();
+ }
+ String description;
+ if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ description = getWatchedString(watchedPositionMs, recording.getDurationMillis());
+ mWatchedRecordings.add(recording.getId());
+ } else {
+ description = getString(R.string.dvr_series_never_watched);
+ }
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(recording.getId())
+ .title(title)
+ .description(description)
+ .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
+ .build());
+ }
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_DELETE)
+ .title(getString(R.string.dvr_detail_delete))
+ .build());
+ actions.add(new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_ID_DELETE) {
+ int deletionCount = 0;
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
+ && guidedAction.isChecked()) {
+ dvrManager.removeRecordedProgram(guidedAction.getId());
+ deletionCount++;
+ }
+ }
+ Toast.makeText(getContext(), getResources().getQuantityString(
+ R.plurals.dvr_msg_episodes_deleted, deletionCount, deletionCount,
+ mRecordings.size()), Toast.LENGTH_LONG).show();
+ finishGuidedStepFragments();
+ } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ finishGuidedStepFragments();
+ } else if (actionId == ACTION_ID_SELECT_WATCHED) {
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ long recordingId = guidedAction.getId();
+ if (mWatchedRecordings.contains(recordingId)) {
+ guidedAction.setChecked(true);
+ } else {
+ guidedAction.setChecked(false);
+ }
+ notifyActionChanged(findActionPositionById(recordingId));
+ }
+ }
+ mAllSelected = updateSelectAllState();
+ } else if (actionId == ACTION_ID_SELECT_ALL) {
+ mAllSelected = !mAllSelected;
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ guidedAction.setChecked(mAllSelected);
+ notifyActionChanged(findActionPositionById(guidedAction.getId()));
+ }
+ }
+ updateSelectAllState(action, mAllSelected);
+ } else {
+ mAllSelected = updateSelectAllState();
+ }
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ return new DvrGuidedActionsStylist(true);
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylistWithDivider() {
+ @Override
+ public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
+ super.onBindViewHolder(vh, action);
+ if (action.getId() == ACTION_DIVIDER) {
+ return;
+ }
+ LayoutParams lp = vh.itemView.getLayoutParams();
+ if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ lp.height = mOneLineActionHeight;
+ } else {
+ vh.itemView.setLayoutParams(
+ new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT));
+ }
+ }
+ };
+ }
+
+ private String getWatchedString(long watchedPositionMs, long durationMs) {
+ if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
+ return getResources().getString(R.string.dvr_series_watched_info_minutes,
+ Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)),
+ TimeUnit.MILLISECONDS.toMinutes(durationMs));
+ } else {
+ return getResources().getString(R.string.dvr_series_watched_info_seconds,
+ Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)),
+ TimeUnit.MILLISECONDS.toSeconds(durationMs));
+ }
+ }
+
+ private boolean updateSelectAllState() {
+ for (GuidedAction guidedAction : getActions()) {
+ if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
+ if (!guidedAction.isChecked()) {
+ if (mAllSelected) {
+ updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false);
+ }
+ return false;
+ }
+ }
+ }
+ if (!mAllSelected) {
+ updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true);
+ }
+ return true;
+ }
+
+ private void updateSelectAllState(GuidedAction selectAll, boolean select) {
+ selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all)
+ : getString(R.string.dvr_series_select_all));
+ notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL));
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
new file mode 100644
index 00000000..0156e9d9
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.text.TextUtils;
+
+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.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.SeriesRecording;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * {@link DetailsFragment} for series recording in DVR.
+ */
+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 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 DetailsContent mDetailsContent;
+ private int mSeasonRowCount;
+ private SparseArrayObjectAdapter mActionsAdapter;
+ private Action mDeleteAction;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
+ super.onCreate(savedInstanceState);
+ setDetailsOverviewRow(mDetailsContent);
+ setupRecordedProgramsRow();
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mDvrDataManager.addRecordedProgramListener(this);
+ mRecordedPrograms = null;
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager()
+ .getSeriesRecording(recordId);
+ if (mSeries == null) {
+ return false;
+ }
+ mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId());
+ Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR);
+ mDetailsContent = createDetailsContent();
+ return true;
+ }
+
+ @Override
+ protected PresenterSelector onCreatePresenterSelector(
+ DetailsOverviewRowPresenter rowPresenter) {
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+ presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());
+ return presenterSelector;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mSeries.getChannelId());
+ String description = TextUtils.isEmpty(mSeries.getLongDescription())
+ ? mSeries.getDescription() : mSeries.getLongDescription();
+ return new DetailsContent.Builder()
+ .setTitle(mSeries.getTitle())
+ .setDescription(description)
+ .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel)
+ .build();
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ 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)));
+ mDeleteAction = new Action(ACTION_DELETE,
+ getString(R.string.dvr_detail_series_delete), null,
+ res.getDrawable(R.drawable.ic_delete_32dp, null));
+ if (!mRecordedPrograms.isEmpty()) {
+ mActionsAdapter.set(ACTION_DELETE, mDeleteAction);
+ }
+ return mActionsAdapter;
+ }
+
+ private void setupRecordedProgramsRow() {
+ for (RecordedProgram program : mRecordedPrograms) {
+ addProgram(program);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ 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());
+ }
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_SERIES_SCHEDULES) {
+ DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries);
+ } else if (action.getId() == ACTION_DELETE) {
+ DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId());
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (mSeries.getId() == series.getId()) {
+ mSeries = series;
+ // TODO: change action label.
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { }
+
+ @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);
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ // 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);
+ }
+ }
+ }
+ }
+ }
+
+ private void addProgram(RecordedProgram program) {
+ String programSeasonNumber =
+ TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber();
+ getOrCreateSeasonRowAdapter(programSeasonNumber).add(program);
+ }
+
+ private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) {
+ ListRow row = getSeasonRow(seasonNumber, true);
+ return (SeasonRowAdapter) row.getAdapter();
+ }
+
+ private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) {
+ seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber;
+ ArrayObjectAdapter rowsAdaptor = getRowsAdapter();
+ for (int i = rowsAdaptor.size() - 1; i >= 0; i--) {
+ Object row = rowsAdaptor.get(i);
+ if (row instanceof ListRow) {
+ int compareResult = RecordedProgram.numberCompare(seasonNumber,
+ ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber);
+ if (compareResult == 0) {
+ return (ListRow) row;
+ } else if (compareResult < 0) {
+ return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null;
+ }
+ }
+ }
+ return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null;
+ }
+
+ private ListRow createNewSeasonRow(String seasonNumber, int position) {
+ String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle()
+ : 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));
+ ListRow row = new ListRow(header, new SeasonRowAdapter(selector,
+ new Comparator<RecordedProgram>() {
+ @Override
+ public int compare(RecordedProgram lhs, RecordedProgram rhs) {
+ return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs);
+ }
+ }, seasonNumber));
+ getRowsAdapter().add(position, row);
+ return row;
+ }
+
+ private class SeasonRowAdapter extends SortedArrayAdapter<RecordedProgram> {
+ private String mSeasonNumber;
+
+ SeasonRowAdapter(PresenterSelector selector, Comparator<RecordedProgram> comparator,
+ String seasonNumber) {
+ super(selector, comparator);
+ mSeasonNumber = seasonNumber;
+ }
+
+ @Override
+ public long getId(RecordedProgram program) {
+ return program.getId();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
new file mode 100644
index 00000000..d2f26dd1
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
@@ -0,0 +1,238 @@
+/*
+ * 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.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.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.DvrWatchedPositionManager;
+import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+
+import java.util.List;
+
+/**
+ * Presents a {@link SeriesRecording} in the {@link DvrBrowseFragment}.
+ */
+public class SeriesRecordingPresenter extends Presenter {
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrDataManager mDvrDataManager;
+ private final DvrWatchedPositionManager mWatchedPositionManager;
+
+ private static final class SeriesRecordingViewHolder extends ViewHolder implements
+ WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener {
+ private SeriesRecording mSeriesRecording;
+ private RecordingCardView mCardView;
+ private DvrDataManager mDvrDataManager;
+ private DvrWatchedPositionManager mWatchedPositionManager;
+
+ SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager,
+ DvrWatchedPositionManager watchedPositionManager) {
+ super(view);
+ mCardView = view;
+ mDvrDataManager = dvrDataManager;
+ mWatchedPositionManager = watchedPositionManager;
+ }
+
+ @Override
+ public void onWatchedPositionChanged(long recordedProgramId, long positionMs) {
+ if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.removeListener(this, recordedProgramId);
+ updateCardViewContent();
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduledRecording : scheduledRecordings) {
+ if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ updateCardViewContent();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduledRecording : scheduledRecordings) {
+ if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ updateCardViewContent();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
+ if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ 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());
+ }
+ updateCardViewContent();
+ }
+ }
+
+ @Override
+ public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ // Do nothing
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ // Do nothing
+ }
+
+ public void onBound(SeriesRecording seriesRecording) {
+ mSeriesRecording = seriesRecording;
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addRecordedProgramListener(this);
+ for (RecordedProgram recordedProgram :
+ mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ }
+ }
+ updateCardViewContent();
+ }
+
+ public void onUnbound() {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ mWatchedPositionManager.removeListener(this);
+ }
+
+ private void updateCardViewContent() {
+ int count = 0;
+ int quantityStringID;
+ List<RecordedProgram> recordedPrograms =
+ mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId());
+ if (recordedPrograms.size() == 0) {
+ count = mDvrDataManager.getScheduledRecordings(mSeriesRecording.getId()).size();
+ quantityStringID = R.plurals.dvr_count_scheduled_recordings;
+ } else {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ count = recordedPrograms.size();
+ quantityStringID = R.plurals.dvr_count_recordings;
+ } else {
+ quantityStringID = R.plurals.dvr_count_new_recordings;
+ }
+ }
+ mCardView.setContent(mCardView.getResources()
+ .getQuantityString(quantityStringID, count, count), null);
+ }
+ }
+
+ public SeriesRecordingPresenter(Context context) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mChannelDataManager = singletons.getChannelDataManager();
+ mDvrDataManager = singletons.getDvrDataManager();
+ mWatchedPositionManager = singletons.getDvrWatchedPositionManager();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new SeriesRecordingViewHolder(view, mDvrDataManager, mWatchedPositionManager);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder;
+ final SeriesRecording seriesRecording = (SeriesRecording) o;
+ 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);
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ ((RecordingCardView) viewHolder.view).reset();
+ ((SeriesRecordingViewHolder) viewHolder).onUnbound();
+ }
+
+ private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) {
+ cardView.setTitle(recording.getTitle());
+ if (recording.getPosterUri() != null) {
+ cardView.setImageUri(recording.getPosterUri(), false);
+ } else {
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+ String imageUri = null;
+ if (channel != null) {
+ imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString();
+ }
+ cardView.setImageUri(imageUri, true);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
new file mode 100644
index 00000000..c550935c
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
@@ -0,0 +1,236 @@
+/*
+ * 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.FragmentManager;
+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 com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.SeriesRecording.ChannelOption;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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 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;
+
+ private DvrDataManager mDvrDataManager;
+ private SeriesRecording mSeriesRecording;
+ private Channel mChannel;
+ private long mSeriesRecordingId;
+ @ChannelOption int mChannelOption;
+
+ private String mFragmentTitle;
+ private String mProrityActionTitle;
+ private String mProrityActionHighestText;
+ private String mProrityActionLowestText;
+ private String mChannelsActionTitle;
+ private String mChannelsActionAllText;
+
+ private GuidedAction mPriorityGuidedAction;
+ private GuidedAction mChannelsGuidedAction;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mSeriesRecordingId = getArguments().getLong(SERIES_RECORDING_ID);
+ mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mChannelOption = mSeriesRecording.getChannelOption();
+ mChannel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(mSeriesRecording.getChannelId());
+ // TODO: Handle when channel is null.
+ 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);
+ mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest);
+ mChannelsActionTitle = getString(R.string.dvr_series_settings_channels);
+ mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mDvrDataManager.removeSeriesRecordingListener(this);
+ }
+
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String breadcrumb = mSeriesRecording.getTitle();
+ String title = mFragmentTitle;
+ return new Guidance(title, null, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ mPriorityGuidedAction = new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_PRIORITY)
+ .title(mProrityActionTitle)
+ .build();
+ 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)
+ .build();
+ actions.add(mChannelsGuidedAction);
+ updateChannelsGuidedAction(false);
+ }
+
+ @Override
+ public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_OK)
+ .build());
+ actions.add(new GuidedAction.Builder(getActivity())
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == GuidedAction.ACTION_ID_OK) {
+ if (mChannelOption != mSeriesRecording.getChannelOption()) {
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording)
+ .setChannelOption(mChannelOption)
+ .build());
+ }
+ finishGuidedStepFragments();
+ } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ finishGuidedStepFragments();
+ } else if (actionId == ACTION_ID_PRIORITY) {
+ FragmentManager fragmentManager = getFragmentManager();
+ PrioritySettingsFragment fragment = new PrioritySettingsFragment();
+ Bundle args = new Bundle();
+ args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID,
+ mSeriesRecording.getId());
+ fragment.setArguments(args);
+ GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame);
+ }
+ }
+
+ @Override
+ public boolean onSubGuidedActionClicked(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == SUB_ACTION_ID_CHANNEL_ALL) {
+ mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL;
+ updateChannelsGuidedAction(true);
+ return true;
+ } else if (actionId == SUB_ACTION_ID_CHANNEL_ONE) {
+ mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE;
+ updateChannelsGuidedAction(true);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateButtonActionsStylist() {
+ return new DvrGuidedActionsStylist(true);
+ }
+
+ private void updateChannelsGuidedAction(boolean notifyActionChanged) {
+ if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) {
+ mChannelsGuidedAction.setDescription(mChannelsActionAllText);
+ } else {
+ mChannelsGuidedAction.setDescription(mChannel.getDisplayText());
+ }
+ if (notifyActionChanged) {
+ notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL));
+ }
+ }
+
+ private void updatePriorityGuidedAction(boolean notifyActionChanged) {
+ int totalSeriesCount = 0;
+ int priorityOrder = 0;
+ for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) {
+ if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL
+ || seriesRecording.getId() == mSeriesRecording.getId()) {
+ ++totalSeriesCount;
+ }
+ if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL
+ && seriesRecording.getId() != mSeriesRecording.getId()
+ && seriesRecording.getPriority() > mSeriesRecording.getPriority()) {
+ ++priorityOrder;
+ }
+ }
+ if (priorityOrder == 0) {
+ mPriorityGuidedAction.setDescription(mProrityActionHighestText);
+ } else if (priorityOrder >= totalSeriesCount - 1) {
+ mPriorityGuidedAction.setDescription(mProrityActionLowestText);
+ } else {
+ mPriorityGuidedAction.setDescription(Integer.toString(priorityOrder + 1));
+ }
+ if (notifyActionChanged) {
+ notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ if (seriesRecording.getId() == mSeriesRecordingId) {
+ mSeriesRecording = seriesRecording;
+ updatePriorityGuidedAction(true);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
index 8a8bcdeb..3a57d72e 100644
--- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -16,7 +16,8 @@
package com.android.tv.dvr.ui;
-import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.annotation.VisibleForTesting;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.PresenterSelector;
import java.util.ArrayList;
@@ -26,168 +27,152 @@ import java.util.Comparator;
import java.util.List;
/**
- * Keeps a set of {@code T} items sorted, but leaving a {@link EmptyHolder}
- * if there is no items.
+ * Keeps a set of items sorted
*
* <p>{@code T} must have stable IDs.
*/
-abstract class SortedArrayAdapter<T> extends ObjectAdapter {
- private final List<T> mItems = new ArrayList<>();
+public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
private final Comparator<T> mComparator;
+ private final int mMaxItemCount;
+ private int mExtraItemCount;
SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
- super(presenterSelector);
- mComparator = comparator;
- setHasStableIds(true);
- }
-
- @Override
- public final int size() {
- return mItems.isEmpty() ? 1 : mItems.size();
- }
-
- @Override
- public final Object get(int position) {
- return isEmpty() ? EmptyHolder.EMPTY_HOLDER : getItem(position);
+ this(presenterSelector, comparator, Integer.MAX_VALUE);
}
- @Override
- public final long getId(int position) {
- if (isEmpty()) {
- return NO_ID;
- }
- T item = mItems.get(position);
- return item == null ? NO_ID : getId(item);
- }
-
- /**
- * Returns the id of the the given {@code item}.
- *
- * The id must be stable.
- */
- abstract long getId(T item);
-
- /**
- * Returns the item at the given {@code position}.
- *
- * @throws IndexOutOfBoundsException if the position is out of range
- * (<tt>position &lt; 0 || position &gt;= size()</tt>)
- */
- final T getItem(int position) {
- return mItems.get(position);
+ SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator,
+ int maxItemCount) {
+ super(presenterSelector);
+ mComparator = comparator;
+ mMaxItemCount = maxItemCount;
}
/**
- * Returns {@code true} if the list of items is empty.
+ * Sets the objects in the given collection to the adapter keeping the elements sorted.
*
- * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and
- * {@link EmptyHolder#EMPTY_HOLDER} at position 0;
+ * @param items A {@link Collection} of items to be set.
*/
- final boolean isEmpty() {
- return mItems.isEmpty();
+ @VisibleForTesting
+ final void setInitialItems(List<T> items) {
+ List<T> itemsCopy = new ArrayList<>(items);
+ Collections.sort(itemsCopy, mComparator);
+ addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size())));
}
/**
- * Removes all elements from the list.
+ * Adds an item in sorted order to the adapter.
*
- * <p><b>NOTE</b> when the item list is empty the adapter has a size of 1 and
- * {@link EmptyHolder#EMPTY_HOLDER} at position 0;
+ * @param item The item to add in sorted order to the adapter.
*/
- final void clear() {
- mItems.clear();
- notifyChanged();
+ @Override
+ public final void add(Object item) {
+ add((T) item, false);
}
- /**
- * Adds the objects in the given collection to the adapter keeping the elements sorted.
- * If the index is >= {@link #size} an exception will be thrown.
- *
- * @param items A {@link Collection} of items to insert.
- */
- final void addAll(Collection<T> items) {
- mItems.addAll(items);
- Collections.sort(mItems, mComparator);
- notifyChanged();
+ public boolean isEmpty() {
+ return size() == 0;
}
/**
* Adds an item in sorted order to the adapter.
*
* @param item The item to add in sorted order to the adapter.
+ * @param insertToEnd If items are inserted in a more or less sorted fashion,
+ * sets this parameter to {@code true} to search insertion position from
+ * the end to save search time.
*/
- final void add(T item) {
- int i = findWhereToInsert(item);
- mItems.add(i, item);
- if (mItems.size() == 1) {
- notifyItemRangeChanged(0, 1);
+ public final void add(T item, boolean insertToEnd) {
+ int i;
+ if (insertToEnd) {
+ i = findInsertPosition(item);
} else {
- notifyItemRangeInserted(i, 1);
+ i = findInsertPositionBinary(item);
+ }
+ super.add(i, item);
+ if (size() > mMaxItemCount + mExtraItemCount) {
+ removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount);
}
}
/**
- * Remove an item from the list
- *
- * @param item The item to remove from the adapter.
+ * Adds an extra item to the end of the adapter. The items will not be subjected to the sorted
+ * order or the maximum number of items. One or more extra items can be added to the adapter.
+ * They will be presented in their insertion order.
*/
- final void remove(T item) {
- int index = indexOf(item);
- if (index != -1) {
- mItems.remove(index);
- if (mItems.isEmpty()) {
- notifyItemRangeChanged(0, 1);
- } else {
- notifyItemRangeRemoved(index, 1);
- }
- }
+ public int addExtraItem(T item) {
+ super.add(item);
+ return ++mExtraItemCount;
+ }
+
+ /**
+ * Removes an item which has the same ID as {@code item}.
+ */
+ public boolean removeWithId(T item) {
+ int index = indexWithTypeAndId(item);
+ return index >= 0 && index < size() && remove(get(index));
}
/**
* Change an item in the list.
* @param item The item to change.
*/
- final void change(T item) {
- int oldIndex = indexOf(item);
+ public final void change(T item) {
+ int oldIndex = indexWithTypeAndId(item);
if (oldIndex != -1) {
- T old = mItems.get(oldIndex);
+ T old = (T) get(oldIndex);
if (mComparator.compare(old, item) == 0) {
- mItems.set(oldIndex, item);
- notifyItemRangeChanged(oldIndex, 1);
+ replace(oldIndex, item);
return;
}
- mItems.remove(oldIndex);
- }
- int newIndex = findWhereToInsert(item);
- mItems.add(newIndex, item);
-
- if (oldIndex != -1) {
- notifyItemRangeRemoved(oldIndex, 1);
- }
- if (newIndex != -1) {
- notifyItemRangeInserted(newIndex, 1);
+ removeItems(oldIndex, 1);
}
+ add(item);
}
- private int indexOf(T item) {
+ /**
+ * Returns the id of the the given {@code item}, which will be used in {@link #change} to
+ * decide if the given item is already existed in the adapter.
+ *
+ * The id must be stable.
+ */
+ abstract long getId(T item);
+
+ private int indexWithTypeAndId(T item) {
long id = getId(item);
- for (int i = 0; i < mItems.size(); i++) {
- T r = mItems.get(i);
- if (getId(r) == id) {
+ for (int i = 0; i < size() - mExtraItemCount; i++) {
+ T r = (T) get(i);
+ if (r.getClass() == item.getClass() && getId(r) == id) {
return i;
}
}
return -1;
}
- private int findWhereToInsert(T item) {
- int i;
- int size = mItems.size();
- for (i = 0; i < size; i++) {
- T r = mItems.get(i);
- if (mComparator.compare(r, item) > 0) {
- return i;
+ private int findInsertPosition(T item) {
+ for (int i = size() - mExtraItemCount - 1; i >=0; i--) {
+ T r = (T) get(i);
+ if (mComparator.compare(r, item) <= 0) {
+ return i + 1;
+ }
+ }
+ return 0;
+ }
+
+ private int findInsertPositionBinary(T item) {
+ int lb = 0;
+ int ub = size() - mExtraItemCount - 1;
+ while (lb <= ub) {
+ int mid = (lb + ub) / 2;
+ T r = (T) get(mid);
+ int compareResult = mComparator.compare(item, r);
+ if (compareResult == 0) {
+ return mid;
+ } else if (compareResult > 0) {
+ lb = mid + 1;
+ } else {
+ ub = mid - 1;
}
}
- return size;
+ return lb;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
new file mode 100644
index 00000000..61de5764
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
@@ -0,0 +1,209 @@
+/*
+* 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.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+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.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.ScheduledRecording;
+
+/**
+ * A base fragment to show the list of schedule recordings.
+ */
+public abstract class BaseDvrSchedulesFragment extends DetailsFragment
+ implements DvrDataManager.ScheduledRecordingListener,
+ SchedulesHeaderRowPresenter.SchedulesHeaderRowListener,
+ ScheduleRowPresenter.ScheduleRowClickListener {
+ /**
+ * 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;
+
+ @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);
+ mRowsAdapter = onCreateRowsAdapter(presenterSelector);
+ setAdapter(mRowsAdapter);
+ mRowsAdapter.start();
+ TvApplication.getSingletons(getContext()).getDvrDataManager()
+ .addScheduledRecordingListener(this);
+ }
+
+ /**
+ * Returns rows adapter.
+ */
+ protected ScheduleRowAdapter getRowsAdapter() {
+ return mRowsAdapter;
+ }
+
+ /**
+ * 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);
+ }
+
+ @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;
+ }
+
+ @Override
+ public View onInflateTitleView(LayoutInflater inflater, ViewGroup parent,
+ Bundle savedInstanceState) {
+ // Workaround of b/31046014
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ TvApplication.getSingletons(getContext()).getDvrDataManager()
+ .removeScheduledRecordingListener(this);
+ mHeaderRowPresenter.removeListener(this);
+ mRowPresenter.removeListener(this);
+ mRowsAdapter.stop();
+ super.onDestroy();
+ }
+
+ /**
+ * Creates header row presenter.
+ */
+ public abstract SchedulesHeaderRowPresenter onCreateHeaderRowPresenter();
+
+ /**
+ * Creates rows presenter.
+ */
+ public abstract ScheduleRowPresenter onCreateRowPresenter();
+
+ /**
+ * Creates rows adapter.
+ */
+ public abstract ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor);
+
+ /**
+ * 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;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (mRowPresenter != null) {
+ mRowPresenter.onScheduledRecordingAdded(recording);
+ }
+ if (mRowsAdapter != null) {
+ mRowsAdapter.onScheduledRecordingAdded(recording);
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (mRowPresenter != null) {
+ mRowPresenter.onScheduledRecordingRemoved(recording);
+ }
+ if (mRowsAdapter != null) {
+ mRowsAdapter.onScheduledRecordingRemoved(recording);
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ if (mRowPresenter != null) {
+ mRowPresenter.onScheduledRecordingUpdated(recording);
+ }
+ if (mRowsAdapter != null) {
+ mRowsAdapter.onScheduledRecordingUpdated(recording);
+ }
+ }
+ }
+
+ @Override
+ public void onUpdateAllScheduleRows() {
+ if (getRowsAdapter() != null) {
+ getRowsAdapter().notifyArrayItemRangeChanged(0, getRowsAdapter().size());
+ }
+ }
+
+ @Override
+ public void onDeleteClicked(ScheduleRow scheduleRow) {
+ if (mRowsAdapter != null) {
+ mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java
new file mode 100644
index 00000000..c906c62a
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFocusView.java
@@ -0,0 +1,88 @@
+/*
+ * 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 android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.tv.R;
+
+/**
+ * A view used for focus in schedules list.
+ */
+public class DvrSchedulesFocusView extends View {
+ private final Paint mPaint;
+ private final RectF mRoundRectF = new RectF();
+ private final int mRoundRectRadius;
+
+ private final String mViewTag;
+ private final String mHeaderFocusViewTag;
+ private final String mItemFocusViewTag;
+
+ public DvrSchedulesFocusView(Context context) {
+ this(context, null, 0);
+ }
+
+ public DvrSchedulesFocusView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DvrSchedulesFocusView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mHeaderFocusViewTag = getContext().getString(R.string.dvr_schedules_header_focus_view);
+ mItemFocusViewTag = getContext().getString(R.string.dvr_schedules_item_focus_view);
+ mViewTag = (String) getTag();
+ mPaint = createPaint(context);
+ mRoundRectRadius = getRoundRectRadius();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (TextUtils.equals(mViewTag, mHeaderFocusViewTag)) {
+ mRoundRectF.set(0, 0, getWidth(), getHeight());
+ } else if (TextUtils.equals(mViewTag, mItemFocusViewTag)) {
+ int drawHeight = 2 * mRoundRectRadius;
+ int drawOffset = (drawHeight - getHeight()) / 2;
+ mRoundRectF.set(0, -drawOffset, getWidth(), getHeight() + drawOffset);
+ }
+ canvas.drawRoundRect(mRoundRectF, mRoundRectRadius, mRoundRectRadius, mPaint);
+ }
+
+ private Paint createPaint(Context context) {
+ Paint paint = new Paint();
+ paint.setColor(context.getColor(R.color.dvr_schedules_list_item_selector));
+ return paint;
+ }
+
+ private int getRoundRectRadius() {
+ if (TextUtils.equals(mViewTag, mHeaderFocusViewTag)) {
+ return getResources().getDimensionPixelSize(
+ R.dimen.dvr_schedules_header_selector_radius);
+ } else if (TextUtils.equals(mViewTag, mItemFocusViewTag)) {
+ return getResources().getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius);
+ }
+ return 0;
+ }
+}
+
+
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
new file mode 100644
index 00000000..f361ede3
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
@@ -0,0 +1,51 @@
+/*
+ * 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.os.Bundle;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+
+import com.android.tv.R;
+import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
+
+/**
+ * A fragment to show the list of schedule recordings.
+ */
+public class DvrSchedulesFragment extends BaseDvrSchedulesFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getRowsAdapter().size() == 0) {
+ showEmptyMessage(R.string.dvr_schedules_empty_state);
+ }
+ }
+
+ @Override
+ public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() {
+ return new DateHeaderRowPresenter(getContext());
+ }
+
+ @Override
+ public ScheduleRowPresenter onCreateRowPresenter() {
+ return new ScheduleRowPresenter(getContext());
+ }
+
+ @Override
+ public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor) {
+ return new ScheduleRowAdapter(getContext(), presenterSelecor);
+ }
+} \ 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
new file mode 100644
index 00000000..ba8b0c36
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -0,0 +1,112 @@
+/*
+* 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.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.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+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;
+
+/**
+ * A fragment to show the list of series schedule recordings.
+ */
+public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
+ /**
+ * The key for series recording whose scheduled recording list will be displayed.
+ */
+ public static String SERIES_SCHEDULES_KEY_SERIES_RECORDING =
+ "series_schedules_key_series_recording";
+
+ private static String TAG = "DvrSeriesSchedulesFragment";
+
+ private SeriesRecording mSeries;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSeries = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ }
+ 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();
+ }
+ });
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @Override
+ public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() {
+ return new SeriesRecordingHeaderRowPresenter(getContext());
+ }
+
+ @Override
+ public ScheduleRowPresenter onCreateRowPresenter() {
+ return new SeriesScheduleRowPresenter(getContext());
+ }
+
+ @Override
+ public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelector) {
+ return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeries);
+ }
+
+ @Override
+ protected int getFirstItemPosition() {
+ if (mSeries != null && mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED) {
+ return -1;
+ }
+ return super.getFirstItemPosition();
+ }
+
+ @Override
+ public void onDestroy() {
+ ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(null);
+ super.onDestroy();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
new file mode 100644
index 00000000..1e258d2d
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
@@ -0,0 +1,69 @@
+/*
+ * 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 com.android.tv.dvr.ScheduledRecording;
+
+/**
+ * A class for schedule recording row.
+ */
+public class ScheduleRow {
+ private ScheduledRecording mRecording;
+ private boolean mRemoveScheduleChecked;
+ private SchedulesHeaderRow mHeaderRow;
+
+ public ScheduleRow(ScheduledRecording recording, SchedulesHeaderRow headerRow) {
+ mRecording = recording;
+ mRemoveScheduleChecked = false;
+ mHeaderRow = headerRow;
+ }
+
+ /**
+ * Sets scheduled recording.
+ */
+ public void setRecording(ScheduledRecording recording) {
+ mRecording = recording;
+ }
+
+ /**
+ * Sets remove schedule checked status.
+ */
+ public void setRemoveScheduleChecked(boolean checked) {
+ mRemoveScheduleChecked = checked;
+ }
+
+ /**
+ * Gets scheduled recording.
+ */
+ public ScheduledRecording getRecording() {
+ return mRecording;
+ }
+
+ /**
+ * Gets remove schedule checked status.
+ */
+ public boolean isRemoveScheduleChecked() {
+ return mRemoveScheduleChecked;
+ }
+
+ /**
+ * Gets which {@link SchedulesHeaderRow} this schedule row belongs to.
+ */
+ public SchedulesHeaderRow getHeaderRow() {
+ return mHeaderRow;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
new file mode 100644
index 00000000..3e2630c7
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * 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 android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.text.format.DateUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An adapter for {@link ScheduleRow}.
+ */
+public class ScheduleRowAdapter extends ArrayObjectAdapter {
+ private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
+
+ private Context mContext;
+ private final List<String> mTitles = new ArrayList<>();
+
+ public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) {
+ super(classPresenterSelector);
+ mContext = context;
+ mTitles.add(mContext.getString(R.string.dvr_date_today));
+ mTitles.add(mContext.getString(R.string.dvr_date_tomorrow));
+ }
+
+ /**
+ * Returns context.
+ */
+ protected Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Starts schedule row adapter.
+ */
+ public void start() {
+ clear();
+ List<ScheduledRecording> recordingList = TvApplication.getSingletons(mContext)
+ .getDvrDataManager().getNonStartedScheduledRecordings();
+ recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager()
+ .getStartedRecordings());
+ Collections.sort(recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis());
+ for (int i = 0; i < recordingList.size();) {
+ ArrayList<ScheduledRecording> section = new ArrayList<>();
+ while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() < deadLine) {
+ section.add(recordingList.get(i++));
+ }
+ if (!section.isEmpty()) {
+ SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine),
+ mContext.getResources().getQuantityString(
+ R.plurals.dvr_schedules_section_subtitle, section.size(), section.size()),
+ section.size(), deadLine);
+ add(headerRow);
+ for(ScheduledRecording recording : section){
+ add(new ScheduleRow(recording, headerRow));
+ }
+ }
+ deadLine += ONE_DAY_MS;
+ }
+ }
+
+ private String calculateHeaderDate(long deadLine) {
+ int titleIndex = (int) ((deadLine -
+ Utils.getLastMillisecondOfDay(System.currentTimeMillis())) / ONE_DAY_MS);
+ String headerDate;
+ if (titleIndex < mTitles.size()) {
+ 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);
+ }
+ return headerDate;
+ }
+
+ /**
+ * Stops schedules row adapter.
+ */
+ public void stop() {
+ // TODO: Deal with other type of operation.
+ 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());
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to.
+ */
+ public ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) {
+ if (recording == null) {
+ return null;
+ }
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow) {
+ if (((ScheduleRow) item).getRecording().getId() == recording.getId()) {
+ return (ScheduleRow) item;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a {@link ScheduleRow} by {@link ScheduledRecording} and update
+ * {@link SchedulesHeaderRow} information.
+ */
+ protected void addScheduleRow(ScheduledRecording recording) {
+ 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) {
+ break;
+ }
+ pre = index;
+ }
+ }
+ long deadLine = Utils.getLastMillisecondOfDay(recording.getStartTimeMs());
+ if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) {
+ SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow();
+ headerRow.setItemCount(headerRow.getItemCount() + 1);
+ ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
+ add(++pre, addedRow);
+ } 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);
+ } else {
+ SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine),
+ mContext.getResources().getQuantityString(
+ R.plurals.dvr_schedules_section_subtitle, 1, 1), 1, deadLine);
+ add(++pre, headerRow);
+ ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
+ add(pre, addedRow);
+ }
+ }
+ }
+
+ private DateHeaderRow getHeaderRow(int index) {
+ return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
+ }
+
+ /**
+ * Removes {@link ScheduleRow} and update {@link SchedulesHeaderRow} information.
+ */
+ protected void removeScheduleRow(ScheduleRow scheduleRow) {
+ if (scheduleRow != null) {
+ SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
+ remove(scheduleRow);
+ // Changes the count information of header which the removed row belongs to.
+ if (headerRow != null) {
+ int currentCount = headerRow.getItemCount();
+ headerRow.setItemCount(--currentCount);
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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());
+ }
+ }
+
+ /**
+ * 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);
+ }
+ 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);
+ }
+ } else if (willBeKept(recording)) {
+ addScheduleRow(recording);
+ }
+ notifyArrayItemRangeChanged(0, size());
+ }
+
+ /**
+ * 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;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
new file mode 100644
index 00000000..23aaf4c3
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -0,0 +1,760 @@
+/*
+ * 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.animation.ValueAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvInputInfo;
+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.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A RowPresenter for {@link ScheduleRow}.
+ */
+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 final String mTunerConflictWillNotBeRecordedInfo;
+ private final String mTunerConflictWillBePartiallyRecordedInfo;
+ private final String mInfoSeparator;
+
+ /**
+ * A ViewHolder for {@link ScheduleRow}
+ */
+ public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder {
+ private boolean mLtr;
+ private LinearLayout mInfoContainer;
+ private RelativeLayout mScheduleActionContainer;
+ private RelativeLayout mDeleteActionContainer;
+ 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;
+
+ public ScheduleRowViewHolder(View view) {
+ super(view);
+ 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);
+ 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;
+ }
+
+ /**
+ * Returns time view.
+ */
+ public TextView getTimeView() {
+ return mTimeView;
+ }
+
+ /**
+ * Returns title view.
+ */
+ public TextView getProgramTitleView() {
+ 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()) {
+ 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;
+ } else {
+ targetWidth = mInfoContainer.getWidth() + roundRectRadius;
+ }
+ } else if (mScheduleActionContainer.isFocused()) {
+ if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) {
+ targetWidth = mScheduleActionContainer.getWidth();
+ } else {
+ targetWidth = 2 * roundRectRadius;
+ }
+ } else {
+ targetWidth = mDeleteActionContainer.getWidth() + roundRectRadius;
+ }
+
+ float targetTranslationX;
+ if (mInfoContainer.isFocused()) {
+ targetTranslationX = mLtr ? mInfoContainer.getLeft() - roundRectRadius
+ - mSelectorView.getLeft() :
+ mInfoContainer.getRight() + roundRectRadius - mInfoContainer.getRight();
+ } else if (mScheduleActionContainer.isFocused()) {
+ if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) {
+ targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() -
+ mSelectorView.getLeft()
+ : mScheduleActionContainer.getRight() - mSelectorView.getRight();
+ } else {
+ targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() -
+ (roundRectRadius - mScheduleActionContainer.getWidth() / 2) -
+ mSelectorView.getLeft()
+ : mScheduleActionContainer.getRight() +
+ (roundRectRadius - mScheduleActionContainer.getWidth() / 2) -
+ mSelectorView.getRight();
+ }
+ } else {
+ targetTranslationX = mLtr ? mDeleteActionContainer.getLeft()
+ - mSelectorView.getLeft()
+ : mDeleteActionContainer.getRight() - mSelectorView.getRight();
+ }
+
+ if (mSelectorView.getAlpha() == 0) {
+ mSelectorView.setTranslationX(targetTranslationX);
+ lp.width = targetWidth;
+ mSelectorView.requestLayout();
+ }
+
+ // animate the selector in and to the proper width and translation X.
+ final float deltaWidth = lp.width - targetWidth;
+ mSelectorView.animate().cancel();
+ mSelectorView.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()));
+ mSelectorView.requestLayout();
+ }
+ }).setDuration(animationDuration).setInterpolator(interpolator).start();
+ } else {
+ mSelectorView.animate().cancel();
+ mSelectorView.animate().alpha(0f).setDuration(animationDuration)
+ .setInterpolator(interpolator).start();
+ }
+ }
+
+ /**
+ * Grey out the information body.
+ */
+ public void greyOutInfo() {
+ mTimeView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info_grey, null));
+ mProgramTitleView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info_grey, null));
+ mInfoSeparatorView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info_grey, null));
+ mChannelNameView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info_grey, null));
+ mConflictInfoView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info_grey, null));
+ }
+
+ /**
+ * Reverse grey out operation.
+ */
+ public void whiteBackInfo() {
+ mTimeView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info, null));
+ mProgramTitleView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_main, null));
+ mInfoSeparatorView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info, null));
+ mChannelNameView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info, null));
+ mConflictInfoView.setTextColor(mInfoContainer.getResources().getColor(R.color
+ .dvr_schedules_item_info, null));
+ }
+ }
+
+ public ScheduleRowPresenter(Context context) {
+ 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);
+ 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();
+ }
+
+ @Override
+ public ViewHolder createRowViewHolder(ViewGroup parent) {
+ View view = LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item,
+ parent, false);
+ return onGetScheduleRowViewHolder(view);
+ }
+
+ /**
+ * Returns context.
+ */
+ protected Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 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.
+ */
+ 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;
+ }
+
+ @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.
+ viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onInfoClicked(scheduleRow);
+ }
+ });
+
+ viewHolder.mDeleteActionContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onDeleteClicked(scheduleRow, viewHolder);
+ }
+ });
+
+ viewHolder.mScheduleActionContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ onScheduleClicked(scheduleRow);
+ }
+ });
+
+ viewHolder.mTimeView.setText(onGetRecordingTimeText(recording));
+ Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager()
+ .getChannel(recording.getChannelId());
+ String programInfoText = onGetProgramInfoText(recording);
+ if (TextUtils.isEmpty(programInfoText)) {
+ int durationMins =
+ Math.max((int) TimeUnit.MILLISECONDS.toMinutes(recording.getDuration()), 1);
+ programInfoText = mContext.getResources().getQuantityString(
+ R.plurals.dvr_schedules_recording_duration, durationMins, durationMins);
+ }
+ String channelName = channel != null ? channel.getDisplayName() : null;
+ 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);
+ }
+ } else {
+ if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable);
+ } else {
+ viewHolder.mDeleteActionView.setImageDrawable(mScheduleDrawable);
+ }
+ viewHolder.mProgramTitleView.setTextColor(
+ mContext.getResources().getColor(R.color.dvr_schedules_item_info, null));
+ }
+ viewHolder.mRecording = recording;
+ onBindRowViewHolderInternal(viewHolder, scheduleRow);
+ }
+
+ /**
+ * Returns view holder for schedule row.
+ */
+ protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) {
+ return new ScheduleRowViewHolder(view);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Returns program info text for program title view.
+ */
+ protected String onGetProgramInfoText(ScheduledRecording recording) {
+ if (recording != null) {
+ return recording.getProgramTitle();
+ }
+ return null;
+ }
+
+ /**
+ * 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();
+ }
+ }
+ }
+
+ /**
+ * Updates input schedule map.
+ */
+ private void updateInputScheduleMap() {
+ mInputScheduleMap.clear();
+ List<ScheduledRecording> allRecordings = TvApplication.getSingletons(getContext())
+ .getDvrDataManager().getAvailableScheduledRecordings();
+ for(ScheduledRecording recording : allRecordings) {
+ addScheduledRecordingToMap(recording);
+ }
+ updateConflicts();
+ }
+
+ /**
+ * Updates conflicting scheduled recordings.
+ */
+ 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()));
+ }
+ }
+
+ /**
+ * Adds a scheduled recording to the map, it happens when user undo cancel.
+ */
+ private void addScheduledRecordingToMap(ScheduledRecording recording) {
+ 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) {
+ schedulesMap = new HashMap<>();
+ mInputScheduleMap.put(inputId, schedulesMap);
+ }
+ schedulesMap.put(recording.getId(), recording);
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * Adds a scheduled recording to the map, it happens when user undo cancel.
+ */
+ 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;
+ }
+ schedulesMap.put(recording.getId(), recording);
+ } else {
+ removeScheduledRecordingFromMap(recording);
+ }
+ }
+
+ /**
+ * Called when a scheduled recording is updated in dvr date manager.
+ */
+ public void onScheduledRecordingUpdated(ScheduledRecording recording) {
+ updateScheduledRecordingToMap(recording);
+ updateConflicts();
+ }
+
+ /**
+ * Removes a scheduled recording from the map, it happens when user cancel schedule.
+ */
+ private void removeScheduledRecordingFromMap(ScheduledRecording recording) {
+ 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) {
+ return;
+ }
+ schedulesMap.remove(recording.getId());
+ if (schedulesMap.isEmpty()) {
+ mInputScheduleMap.remove(inputId);
+ }
+ }
+
+ /**
+ * Called when a scheduled recording is removed from dvr date manager.
+ */
+ public void onScheduledRecordingRemoved(ScheduledRecording recording) {
+ removeScheduledRecordingFromMap(recording);
+ updateConflicts();
+ }
+
+ /**
+ * Called when user click Info in {@link ScheduleRow}.
+ */
+ protected void onInfoClicked(ScheduleRow scheduleRow) {
+ DvrUiHelper.startDetailsActivity((Activity) mContext,
+ scheduleRow.getRecording(), null, true);
+ }
+
+ /**
+ * Called when user click schedule in {@link ScheduleRow}.
+ */
+ 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();
+ }
+ }
+ }
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording)
+ .setPriority(maxPriority + 1).build());
+ updateConflicts();
+ }
+
+ /**
+ * Called when user click delete in {@link ScheduleRow}.
+ */
+ 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);
+ }
+
+ 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);
+ }
+ viewHolder.whiteBackInfo();
+ scheduleRow.setRemoveScheduleChecked(false);
+ addScheduledRecordingToMap(recording);
+ }
+ updateConflicts();
+ for (ScheduleRowClickListener l : mListeners) {
+ l.onDeleteClicked(scheduleRow);
+ }
+ }
+
+ /**
+ * Adds {@link ScheduleRowClickListener}.
+ */
+ public void addListener(ScheduleRowClickListener scheduleRowClickListener) {
+ mListeners.add(scheduleRowClickListener);
+ }
+
+ /**
+ * Removes {@link ScheduleRowClickListener}.
+ */
+ public void removeListener(ScheduleRowClickListener
+ scheduleRowClickListener) {
+ mListeners.remove(scheduleRowClickListener);
+ }
+
+ @Override
+ protected void onRowViewSelected(ViewHolder vh, boolean selected) {
+ super.onRowViewSelected(vh, selected);
+ onRowViewSelectedInternal(vh, selected);
+ }
+
+ /**
+ * Internal method for onRowViewSelected, can be customized by subclass.
+ */
+ 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);
+ }
+ }
+ }
+
+ /**
+ * A listener for clicking {@link ScheduleRow}.
+ */
+ public interface ScheduleRowClickListener{
+ /**
+ * To notify other observers that delete button has been clicked.
+ */
+ void onDeleteClicked(ScheduleRow scheduleRow);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
new file mode 100644
index 00000000..d103a533
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
@@ -0,0 +1,140 @@
+/*
+ * 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.support.annotation.Nullable;
+
+import com.android.tv.dvr.SeriesRecording;
+
+/**
+ * A base class for the rows for schedules' header.
+ */
+public abstract class SchedulesHeaderRow {
+ private String mTitle;
+ private String mDescription;
+ private int mItemCount;
+
+ public SchedulesHeaderRow(String title, String description, int itemCount) {
+ mTitle = title;
+ mItemCount = itemCount;
+ mDescription = description;
+ }
+
+ /**
+ * Sets title.
+ */
+ public void setTitle(String title) {
+ mTitle = title;
+ }
+
+ /**
+ * Sets description.
+ */
+ public void setDescription(String description) {
+ mDescription = description;
+ }
+
+ /**
+ * Sets count of items.
+ */
+ public void setItemCount(int itemCount) {
+ mItemCount = itemCount;
+ }
+
+ /**
+ * Returns title.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns description.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns count of items.
+ */
+ public int getItemCount() {
+ return mItemCount;
+ }
+
+ /**
+ * The header row which represent the date.
+ */
+ public static class DateHeaderRow extends SchedulesHeaderRow {
+ private long mDeadLineMs;
+
+ public DateHeaderRow(String title, String description, int itemCount, long deadLineMs) {
+ super(title, description, itemCount);
+ mDeadLineMs = deadLineMs;
+ }
+
+ /**
+ * 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() {
+ return mDeadLineMs;
+ }
+ }
+
+ /**
+ * The header row which represent the series recording.
+ */
+ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow {
+ private SeriesRecording mSeries;
+ private boolean mCancelAllChecked;
+
+ 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;
+ }
+
+ /**
+ * Returns cancel all checked status.
+ */
+ public boolean isCancelAllChecked() {
+ return mCancelAllChecked;
+ }
+
+ /**
+ * Returns the series recording, it is for series schedules list.
+ */
+ public SeriesRecording getSeriesRecording() {
+ return mSeries;
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
new file mode 100644
index 00000000..483962e7
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -0,0 +1,304 @@
+/*
+ * 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.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;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.ui.DvrSchedulesActivity;
+import com.android.tv.dvr.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);
+ setSelectEffectEnabled(false);
+ mContext = context;
+ }
+
+ /**
+ * Returns the context.
+ */
+ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 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 {
+ private TextView mTitle;
+ private TextView mDescription;
+
+ public SchedulesHeaderRowViewHolder(Context context, ViewGroup parent) {
+ super(LayoutInflater.from(context).inflate(R.layout.dvr_schedules_header, parent,
+ false));
+ mTitle = (TextView) view.findViewById(R.id.header_title);
+ mDescription = (TextView) view.findViewById(R.id.header_description);
+ }
+ }
+
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) {
+ super.onBindRowViewHolder(viewHolder, item);
+ SchedulesHeaderRowViewHolder headerViewHolder = (SchedulesHeaderRowViewHolder) viewHolder;
+ SchedulesHeaderRow header = (SchedulesHeaderRow) item;
+ headerViewHolder.mTitle.setText(header.getTitle());
+ headerViewHolder.mDescription.setText(header.getDescription());
+ }
+
+ /**
+ * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ */
+ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter {
+ public DateHeaderRowPresenter(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected ViewHolder createRowViewHolder(ViewGroup parent) {
+ return new DateHeaderRowViewHolder(getContext(), parent);
+ }
+
+ /**
+ * A ViewHolder for
+ * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ */
+ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder {
+ public DateHeaderRowViewHolder(Context context, ViewGroup parent) {
+ super(context, parent);
+ }
+ }
+ }
+
+ /**
+ * A presenter for {@link SeriesRecordingHeaderRow}.
+ */
+ public static class SeriesRecordingHeaderRowPresenter extends SchedulesHeaderRowPresenter {
+ private final boolean mLtr;
+ private final Drawable mSettingsDrawable;
+ private final Drawable mCancelDrawable;
+ private final Drawable mResumeDrawable;
+
+ private final String mSettingsInfo;
+ private final String mCancelAllInfo;
+ private final String mResumeInfo;
+
+ public SeriesRecordingHeaderRowPresenter(Context context) {
+ super(context);
+ mLtr = context.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ mSettingsDrawable = context.getDrawable(R.drawable.ic_settings);
+ 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);
+ }
+
+ @Override
+ protected ViewHolder createRowViewHolder(ViewGroup parent) {
+ return new SeriesRecordingRowViewHolder(getContext(), parent);
+ }
+
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) {
+ super.onBindRowViewHolder(viewHolder, item);
+ SeriesRecordingRowViewHolder headerViewHolder =
+ (SeriesRecordingRowViewHolder) viewHolder;
+ SeriesRecordingHeaderRow header = (SeriesRecordingHeaderRow) item;
+ headerViewHolder.mSeriesSettingsButton.setVisibility(
+ isSeriesScheduleCanceled(getContext(), header) ? View.INVISIBLE : View.VISIBLE);
+ headerViewHolder.mSeriesSettingsButton.setText(mSettingsInfo);
+ setTextDrawable(headerViewHolder.mSeriesSettingsButton, mSettingsDrawable);
+ if (header.isCancelAllChecked()) {
+ headerViewHolder.mTogglePauseButton.setText(mResumeInfo);
+ setTextDrawable(headerViewHolder.mTogglePauseButton, mResumeDrawable);
+ } else {
+ headerViewHolder.mTogglePauseButton.setText(mCancelAllInfo);
+ setTextDrawable(headerViewHolder.mTogglePauseButton, mCancelDrawable);
+ }
+ headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DvrUiHelper.startSeriesSettingsActivity(getContext(),
+ header.getSeriesRecording().getId());
+ }
+ });
+ headerViewHolder.mTogglePauseButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (!header.isCancelAllChecked()) {
+ DvrUiHelper.showCancelAllSeriesRecordingDialog((DvrSchedulesActivity) view
+ .getContext());
+ } else {
+ if (isSeriesScheduleCanceled(getContext(), header)) {
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .updateSeriesRecording(SeriesRecording.buildFrom(header
+ .getSeriesRecording()).setState(SeriesRecording
+ .STATE_SERIES_NORMAL).build());
+ }
+ header.setCancelAllChecked(false);
+ notifyUpdateAllScheduleRows();
+ }
+ }
+ });
+ }
+
+ private void setTextDrawable(TextView textView, Drawable drawableStart) {
+ if (mLtr) {
+ textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null);
+ } else {
+ textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null);
+ }
+ }
+
+ 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 {
+ private final TextView mSeriesSettingsButton;
+ private final TextView mTogglePauseButton;
+ private final boolean mLtr;
+
+ private final View mSelector;
+
+ private View mLastFocusedView;
+ public SeriesRecordingRowViewHolder(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);
+ mSelector = view.findViewById(R.id.selector);
+ OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean focused) {
+ onIconFouseChange(view);
+ }
+ };
+ mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener);
+ mTogglePauseButton.setOnFocusChangeListener(onFocusChangeListener);
+ }
+
+ void onIconFouseChange(View focusedView) {
+ updateSelector(focusedView, mSelector);
+ }
+
+ private void updateSelector(View focusedView, final View selectorView) {
+ int animationDuration = selectorView.getContext().getResources()
+ .getInteger(android.R.integer.config_shortAnimTime);
+ DecelerateInterpolator interpolator = new DecelerateInterpolator();
+
+ if (focusedView.hasFocus()) {
+ final ViewGroup.LayoutParams lp = selectorView.getLayoutParams();
+ final int targetWidth = focusedView.getWidth();
+ float targetTranslationX;
+ if (mLtr) {
+ targetTranslationX = focusedView.getLeft() - selectorView.getLeft();
+ } else {
+ targetTranslationX = focusedView.getRight() - selectorView.getRight();
+ }
+
+ // if the selector is invisible, set the width and translation X directly -
+ // don't animate.
+ if (selectorView.getAlpha() == 0) {
+ selectorView.setTranslationX(targetTranslationX);
+ lp.width = targetWidth;
+ selectorView.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)
+ .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();
+ }
+ }).setDuration(animationDuration).setInterpolator(interpolator).start();
+ mLastFocusedView = focusedView;
+ } else if (mLastFocusedView == focusedView) {
+ selectorView.animate().cancel();
+ selectorView.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
new file mode 100644
index 00000000..8b162c54
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -0,0 +1,156 @@
+/*
+* 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 android.support.v17.leanback.widget.ClassPresenterSelector;
+
+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.SeriesRecording;
+import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An adapter for series schedule row.
+ */
+public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
+ private static final String TAG = "SeriesScheduleRowAdapter";
+
+ private SeriesRecording mSeriesRecording;
+
+ public SeriesScheduleRowAdapter(Context context,
+ ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording) {
+ super(context, classPresenterSelector);
+ mSeriesRecording = seriesRecording;
+ }
+
+ @Override
+ public void start() {
+ List<ScheduledRecording> recordings = TvApplication.getSingletons(getContext())
+ .getDvrDataManager().getAvailableAndCanceledScheduledRecordings();
+ List<ScheduledRecording> seriesScheduledRecordings = new ArrayList<>();
+ if (mSeriesRecording == null) {
+ return;
+ }
+ for (ScheduledRecording recording : recordings) {
+ if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ seriesScheduledRecordings.add(recording);
+ }
+ }
+ 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;
+ }
+ 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));
+ }
+ }
+
+ @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());
+ }
+ }
+ }
+ }
+ }
+
+ @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;
+ }
+ }
+ }
+ 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);
+ }
+ }
+
+ @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);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected boolean willBeKept(ScheduledRecording recording) {
+ return super.willBeKept(recording)
+ || recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED;
+ }
+
+ 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);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
new file mode 100644
index 00000000..4f31528c
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
@@ -0,0 +1,117 @@
+/*
+* 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 android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+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 boolean mLtr;
+
+ public SeriesScheduleRowPresenter(Context context) {
+ super(context);
+ mLtr = context.getResources().getConfiguration().getLayoutDirection()
+ == View.LAYOUT_DIRECTION_LTR;
+ }
+
+ public static class SeriesScheduleRowViewHolder extends ScheduleRowViewHolder {
+ public SeriesScheduleRowViewHolder(View view) {
+ super(view);
+ ViewGroup.LayoutParams lp = getTimeView().getLayoutParams();
+ lp.width = view.getResources().getDimensionPixelSize(
+ R.dimen.dvr_series_schedules_item_time_width);
+ getTimeView().setLayoutParams(lp);
+ }
+ }
+
+ @Override
+ protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) {
+ return new SeriesScheduleRowViewHolder(view);
+ }
+
+ @Override
+ protected String onGetRecordingTimeText(ScheduledRecording recording) {
+ return Utils.getDurationString(getContext(),
+ recording.getStartTimeMs(), recording.getEndTimeMs(), false, true, true, 0);
+ }
+
+ @Override
+ protected String onGetProgramInfoText(ScheduledRecording recording) {
+ if (recording != null) {
+ return recording.getEpisodeDisplayTitle(getContext());
+ }
+ return null;
+ }
+
+ @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) {
+ viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext()
+ .getResources().getDimensionPixelOffset(
+ R.dimen.dvr_schedules_warning_icon_padding));
+ if (mLtr) {
+ viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
+ } else {
+ viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
+ 0, 0, R.drawable.ic_warning_gray600_36dp, 0);
+ }
+ } else {
+ viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ if (mIsCancelAll) {
+ viewHolder.getInfoContainer().setClickable(false);
+ viewHolder.getDeleteActionContainer().setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) {
+ ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
+ if (!mIsCancelAll) {
+ if (selected) {
+ viewHolder.getDeleteActionContainer().setVisibility(View.VISIBLE);
+ } else {
+ viewHolder.getDeleteActionContainer().setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void onInfoClicked(ScheduleRow scheduleRow) {
+ DvrUiHelper.startSchedulesActivity(getContext(), scheduleRow.getRecording());
+ }
+}