aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2016-10-26 14:03:09 -0700
committerNick Chalko <nchalko@google.com>2016-10-31 10:36:49 -0700
commitd41f0075a7d2ea826204e81fcec57d0aa57171a9 (patch)
treecb30cfbafd80e01d314868cdc36e783d39981119 /src
parent5e0ec06a797e3497da94390c63c7072de442695b (diff)
downloadTV-d41f0075a7d2ea826204e81fcec57d0aa57171a9.tar.gz
Sync to ub-tv-killing at 6f6e46557accb62c9548e4177d6005aa944dbf33
Change-Id: I873644d6d9d0110c981ef6075cb4019c16bbb94b
Diffstat (limited to 'src')
-rw-r--r--src/com/android/exoplayer/MediaFormatUtil.java30
-rw-r--r--src/com/android/tv/ApplicationSingletons.java3
-rw-r--r--src/com/android/tv/InputSessionManager.java37
-rw-r--r--src/com/android/tv/MainActivity.java142
-rw-r--r--src/com/android/tv/TimeShiftManager.java52
-rw-r--r--src/com/android/tv/TvApplication.java36
-rw-r--r--src/com/android/tv/data/BaseProgram.java10
-rw-r--r--src/com/android/tv/data/Channel.java7
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java10
-rw-r--r--src/com/android/tv/data/Lineup.java1
-rw-r--r--src/com/android/tv/data/Program.java2
-rw-r--r--src/com/android/tv/data/StreamInfo.java3
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java29
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java55
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java11
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java408
-rw-r--r--src/com/android/tv/dvr/DvrDbSync.java208
-rw-r--r--src/com/android/tv/dvr/DvrManager.java465
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackActivity.java2
-rw-r--r--src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java44
-rw-r--r--src/com/android/tv/dvr/DvrPlayer.java84
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java3
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java439
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java376
-rw-r--r--src/com/android/tv/dvr/DvrUiHelper.java154
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java43
-rw-r--r--src/com/android/tv/dvr/EpisodicProgramLoadTask.java382
-rw-r--r--src/com/android/tv/dvr/InputTaskScheduler.java85
-rw-r--r--src/com/android/tv/dvr/RecordedProgram.java43
-rw-r--r--src/com/android/tv/dvr/RecordingTask.java78
-rw-r--r--src/com/android/tv/dvr/ScheduledRecording.java48
-rw-r--r--src/com/android/tv/dvr/Scheduler.java22
-rw-r--r--src/com/android/tv/dvr/SeriesRecording.java48
-rw-r--r--src/com/android/tv/dvr/SeriesRecordingScheduler.java412
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java15
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java6
-rw-r--r--src/com/android/tv/dvr/ui/DetailsContentPresenter.java284
-rw-r--r--src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java8
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrBrowseFragment.java157
-rw-r--r--src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java65
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsActivity.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrDetailsFragment.java109
-rw-r--r--src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java11
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java88
-rw-r--r--src/com/android/tv/dvr/ui/DvrItemPresenter.java80
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java20
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java12
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java34
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java43
-rw-r--r--src/com/android/tv/dvr/ui/DvrSchedulesActivity.java86
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java1
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java154
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java40
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java57
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java (renamed from src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java)6
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java104
-rw-r--r--src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java37
-rw-r--r--src/com/android/tv/dvr/ui/PrioritySettingsFragment.java7
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java110
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramPresenter.java30
-rw-r--r--src/com/android/tv/dvr/ui/RecordingCardView.java13
-rw-r--r--src/com/android/tv/dvr/ui/RecordingDetailsFragment.java13
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java25
-rw-r--r--src/com/android/tv/dvr/ui/SeriesDeletionFragment.java13
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java162
-rw-r--r--src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java82
-rw-r--r--src/com/android/tv/dvr/ui/SeriesSettingsFragment.java221
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java127
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java35
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java168
-rw-r--r--src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java89
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java176
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java269
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java909
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java34
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java139
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java275
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java98
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java61
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java42
-rw-r--r--src/com/android/tv/guide/ProgramManager.java24
-rw-r--r--src/com/android/tv/guide/ProgramRow.java100
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java92
-rw-r--r--src/com/android/tv/menu/ActionCardView.java2
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java73
-rw-r--r--src/com/android/tv/menu/BaseCardView.java109
-rw-r--r--src/com/android/tv/menu/ChannelCardView.java92
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java7
-rw-r--r--src/com/android/tv/menu/Menu.java7
-rw-r--r--src/com/android/tv/menu/MenuRowView.java19
-rw-r--r--src/com/android/tv/menu/MenuUpdater.java96
-rw-r--r--src/com/android/tv/menu/MenuView.java23
-rw-r--r--src/com/android/tv/menu/PlayControlsButton.java1
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java38
-rw-r--r--src/com/android/tv/menu/SetupCardView.java53
-rw-r--r--src/com/android/tv/menu/SimpleCardView.java10
-rw-r--r--src/com/android/tv/onboarding/NewSourcesFragment.java3
-rw-r--r--src/com/android/tv/onboarding/OnboardingActivity.java28
-rw-r--r--src/com/android/tv/setup/SystemSetupActivity.java124
-rw-r--r--src/com/android/tv/tuner/TunerPreferenceProvider.java2
-rw-r--r--src/com/android/tv/tuner/TunerPreferences.java24
-rw-r--r--src/com/android/tv/tuner/data/TunerChannel.java2
-rw-r--r--src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java57
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java290
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java5
-rw-r--r--src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java283
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java52
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java4
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java147
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java3
-rw-r--r--src/com/android/tv/tuner/exoplayer/SampleExtractor.java15
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java18
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java75
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java17
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java7
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java10
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java22
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java22
-rw-r--r--src/com/android/tv/tuner/setup/ScanFragment.java53
-rw-r--r--src/com/android/tv/tuner/setup/ScanResultFragment.java6
-rw-r--r--src/com/android/tv/tuner/setup/TunerSetupActivity.java20
-rw-r--r--src/com/android/tv/tuner/source/FileTsStreamer.java51
-rw-r--r--src/com/android/tv/tuner/source/TsDataSource.java (renamed from src/com/android/tv/tuner/source/TsMediaDataSource.java)8
-rw-r--r--src/com/android/tv/tuner/source/TsDataSourceManager.java (renamed from src/com/android/tv/tuner/source/TsMediaDataSourceManager.java)37
-rw-r--r--src/com/android/tv/tuner/source/TsStreamWriter.java237
-rw-r--r--src/com/android/tv/tuner/source/TsStreamer.java6
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamer.java83
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamerManager.java14
-rw-r--r--src/com/android/tv/tuner/ts/SectionParser.java7
-rw-r--r--src/com/android/tv/tuner/tvinput/ChannelDataManager.java201
-rw-r--r--src/com/android/tv/tuner/tvinput/EventDetector.java9
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java105
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSession.java12
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSessionWorker.java196
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java166
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerTvInputService.java19
-rw-r--r--src/com/android/tv/tuner/util/GlobalSettingsUtils.java36
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java2
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java56
-rw-r--r--src/com/android/tv/ui/TunableTvView.java23
-rw-r--r--src/com/android/tv/ui/ViewUtils.java50
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java30
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java10
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragmentManager.java12
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java4
-rw-r--r--src/com/android/tv/util/CompositeComparator.java (renamed from src/com/android/tv/tuner/TunerFlags.java)26
-rw-r--r--src/com/android/tv/util/DvrTunerStorageUtils.java113
-rw-r--r--src/com/android/tv/util/LocationUtils.java99
-rw-r--r--src/com/android/tv/util/PermissionUtils.java7
-rw-r--r--src/com/android/tv/util/PipInputManager.java2
-rw-r--r--src/com/android/tv/util/RecurringRunner.java5
-rw-r--r--src/com/android/tv/util/SetupUtils.java43
-rw-r--r--src/com/android/tv/util/SystemProperties.java6
-rw-r--r--src/com/android/tv/util/ToastUtils.java43
-rw-r--r--src/com/android/tv/util/TvSettings.java4
-rw-r--r--src/com/android/tv/util/Utils.java16
162 files changed, 8165 insertions, 3973 deletions
diff --git a/src/com/android/exoplayer/MediaFormatUtil.java b/src/com/android/exoplayer/MediaFormatUtil.java
index 647f7dd9..d7a981f6 100644
--- a/src/com/android/exoplayer/MediaFormatUtil.java
+++ b/src/com/android/exoplayer/MediaFormatUtil.java
@@ -17,6 +17,8 @@ package com.google.android.exoplayer;
import android.support.annotation.Nullable;
+import com.google.android.exoplayer.util.MimeTypes;
+
import java.nio.ByteBuffer;
import java.util.ArrayList;
@@ -29,7 +31,6 @@ public class MediaFormatUtil {
* {@link android.media.MediaFormat} should be converted to be used with ExoPlayer.
*/
public static MediaFormat createMediaFormat(android.media.MediaFormat format) {
- // TODO: Add test for this method.
String mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE);
int maxInputSize =
@@ -52,36 +53,17 @@ public class MediaFormatUtil {
}
long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION)
? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US;
+ int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT
+ : MediaFormat.NO_VALUE;
MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE,
maxInputSize, durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE,
channelCount, sampleRate, language, MediaFormat.OFFSET_SAMPLE_RELATIVE,
- initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE,
- MediaFormat.NO_VALUE, encoderDelay, encoderPadding);
+ initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, pcmEncoding,
+ encoderDelay, encoderPadding, null, MediaFormat.NO_VALUE);
mediaFormat.setFrameworkFormatV16(format);
return mediaFormat;
}
- /**
- * Creates {@link MediaFormat} for audio track.
- */
- public static MediaFormat createAudioMediaFormat(String mimeType, long durationUs,
- int channelCount, int sampleRate) {
- return MediaFormat.createAudioFormat(null, mimeType, MediaFormat.NO_VALUE,
- MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, "");
- }
-
- /**
- * Creates {@link MediaFormat} for closed caption track.
- */
- public static MediaFormat createTextMediaFormat(String mimeType, long durationUs) {
- return new MediaFormat(null, mimeType, 0, MediaFormat.NO_VALUE, durationUs,
- MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE,
- MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, "",
- MediaFormat.OFFSET_SAMPLE_RELATIVE, null, false, MediaFormat.NO_VALUE,
- MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE,
- MediaFormat.NO_VALUE);
- }
-
@Nullable
private static String getOptionalStringV16(android.media.MediaFormat format, String key) {
return format.containsKey(key) ? format.getString(key) : null;
diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/ApplicationSingletons.java
index 3f381635..fd125d52 100644
--- a/src/com/android/tv/ApplicationSingletons.java
+++ b/src/com/android/tv/ApplicationSingletons.java
@@ -24,6 +24,7 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.util.AccountHelper;
import com.android.tv.util.TvInputManagerHelper;
@@ -39,6 +40,8 @@ public interface ApplicationSingletons {
DvrDataManager getDvrDataManager();
+ DvrStorageStatusManager getDvrStorageStatusManager();
+
DvrScheduleManager getDvrScheduleManager();
DvrManager getDvrManager();
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index c3380d81..e4b0f456 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -33,6 +33,7 @@ import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -108,8 +109,8 @@ public class InputSessionManager {
*/
@NonNull
public RecordingSession createRecordingSession(String inputId, String tag,
- RecordingCallback callback, Handler handler) {
- RecordingSession session = new RecordingSession(inputId, tag, callback, handler);
+ RecordingCallback callback, Handler handler, long endTimeMs) {
+ RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
mRecordingSessions.add(session);
if (DEBUG) Log.d(TAG, "Recording session created: " + session);
return session;
@@ -160,6 +161,24 @@ public class InputSessionManager {
return null;
}
+ /**
+ * Retruns the earliest end time of recording sessions in progress of the certain TV input.
+ */
+ @MainThread
+ public Long getEarliestRecordingSessionEndTimeMs(String inputId) {
+ long timeMs = Long.MAX_VALUE;
+ synchronized (mRecordingSessions) {
+ for (RecordingSession session : mRecordingSessions) {
+ if (session.mTuned && TextUtils.equals(inputId, session.mInputId)) {
+ if (session.mEndTimeMs < timeMs) {
+ timeMs = session.mEndTimeMs;
+ }
+ }
+ }
+ }
+ return timeMs == Long.MAX_VALUE ? null : timeMs;
+ }
+
@MainThread
int getTunedTvViewSessionCount(String inputId) {
int tunedCount = 0;
@@ -353,14 +372,17 @@ public class InputSessionManager {
private Uri mChannelUri;
private final RecordingCallback mCallback;
private final Handler mHandler;
+ private volatile long mEndTimeMs;
private TvRecordingClient mClient;
private boolean mTuned;
- RecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler) {
+ RecordingSession(String inputId, String tag, RecordingCallback callback,
+ Handler handler, long endTimeMs) {
mInputId = inputId;
mCallback = callback;
mHandler = handler;
mClient = new TvRecordingClient(mContext, tag, callback, handler);
+ mEndTimeMs = endTimeMs;
}
void release() {
@@ -406,6 +428,7 @@ public class InputSessionManager {
});
return;
}
+ mTuned = true;
int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0
&& tunedRecordingSessionCount + tunedTuneSessionCount
@@ -419,7 +442,6 @@ public class InputSessionManager {
}
}
mChannelUri = channelUri;
- mTuned = true;
mClient.tune(inputId, channelUri);
}
});
@@ -439,6 +461,13 @@ public class InputSessionManager {
mClient.stopRecording();
}
+ /**
+ * Sets recording session's ending time.
+ */
+ public void setEndTimeMs(long endTimeMs) {
+ mEndTimeMs = endTimeMs;
+ }
+
private void runOnHandler(Handler handler, Runnable runnable) {
if (Looper.myLooper() == handler.getLooper()) {
runnable.run();
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index b38b2911..58850b5f 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -94,9 +94,11 @@ import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.ConflictChecker;
-import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment;
import com.android.tv.experiments.Experiments;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
@@ -205,6 +207,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_MUTE);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MUTE);
BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_SEARCH);
+ BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_WINDOW);
}
@@ -231,10 +234,25 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO,
UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK})
private @interface ChannelBannerUpdateReason {}
+ /**
+ * Updates channel banner because the channel banner is forced to show.
+ */
private static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1;
+ /**
+ * Updates channel banner because of tuning.
+ */
private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2;
+ /**
+ * Updates channel banner because of fast tuning.
+ */
private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3;
+ /**
+ * Updates channel banner because of info updating.
+ */
private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4;
+ /**
+ * Updates channel banner because the current watched channel is locked or unlocked.
+ */
private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000;
@@ -256,7 +274,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private final DurationTimer mMainDurationTimer = new DurationTimer();
private final DurationTimer mTuneDurationTimer = new DurationTimer();
private DvrManager mDvrManager;
- private DvrDataManager mDvrDataManager;
private ConflictChecker mDvrConflictChecker;
private View mContentView;
@@ -463,8 +480,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// Check this permission for the EPG fetch.
// TODO: check {@link shouldShowRequestPermissionRationale}.
- if (Utils.hasInternalTvInputs(this, true) && checkSelfPermission(
- android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ // While testing, no way to allow the permission when the dialog shows up. So we'd better
+ // not show the dialog.
+ if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true)
+ && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
@@ -541,7 +560,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mMemoryManageables.add(TvContentRatingCache.getInstance());
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrManager = tvApplication.getDvrManager();
- mDvrDataManager = tvApplication.getDvrDataManager();
}
mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, mTracker,
new OnCurrentProgramUpdatedListener() {
@@ -1179,7 +1197,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* It might be a live program. If the time shifting is available, it can be a past program, too.
*/
public Program getCurrentProgram() {
- if (mTimeShiftManager.isAvailable()) {
+ if (!isChannelChangeKeyDownReceived() && mTimeShiftManager.isAvailable()) {
+ // We shouldn't get current program from TimeShiftManager during channel tunning
return mTimeShiftManager.getCurrentProgram();
}
return mProgramDataManager.getCurrentProgram(getCurrentChannelId());
@@ -2276,6 +2295,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy()");
+ SideFragment.releasePreloadedRecycledViews();
if (mTvView != null) {
mTvView.release();
}
@@ -2354,9 +2374,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case KeyEvent.KEYCODE_DPAD_UP:
if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- moveToAdjacentChannel(true, false);
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED,
System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(true, false);
mTracker.sendChannelUp();
}
return true;
@@ -2364,9 +2386,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.getRepeatCount() == 0
&& mChannelTuner.getBrowsableChannelCount() > 0) {
- moveToAdjacentChannel(false, false);
+ // message sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED,
System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ moveToAdjacentChannel(false, false);
mTracker.sendChannelDown();
}
return true;
@@ -2390,7 +2414,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* S KEYCODE_CAPTIONS: select subtitle
* W debug: toggle screen size
* V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
- * X KEYCODE_BUTTON_X KEYCODE_PROG_BLUE debug: record current channel for a few minutes
*/
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")");
@@ -2492,7 +2515,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
false);
}
return true;
-
+ case KeyEvent.KEYCODE_WINDOW:
+ enterPictureInPictureMode();
+ return true;
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
case KeyEvent.KEYCODE_E:
@@ -2542,8 +2567,61 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mOverlayManager.showBanner();
return true;
}
+ case KeyEvent.KEYCODE_MEDIA_RECORD:
+ case KeyEvent.KEYCODE_V: {
+ Channel currentChannel = getCurrentChannel();
+ if (currentChannel != null && mDvrManager != null) {
+ boolean isRecording =
+ mDvrManager.getCurrentRecording(currentChannel.getId()) != null;
+ if (!isRecording) {
+ if (!mDvrManager.isChannelRecordable(currentChannel)) {
+ Toast.makeText(this, R.string.dvr_msg_cannot_record_program,
+ Toast.LENGTH_SHORT).show();
+ } else {
+ if (!DvrUiHelper.checkStorageStatusAndShowErrorMessage(this,
+ currentChannel.getInputId())) {
+ return true;
+ }
+ Program program = mProgramDataManager
+ .getCurrentProgram(currentChannel.getId());
+ if (program == null) {
+ DvrUiHelper
+ .showChannelRecordDurationOptions(this, currentChannel);
+ } else if (DvrUiHelper.handleCreateSchedule(this, program)) {
+ String msg = getString(
+ R.string.dvr_msg_current_program_scheduled,
+ program.getTitle(), Utils.toTimeString(
+ program.getEndTimeUtcMillis(), false));
+ Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
+ }
+ }
+ } else {
+ DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(),
+ DvrStopRecordingFragment.REASON_USER_STOP,
+ new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
+ ScheduledRecording currentRecording =
+ mDvrManager.getCurrentRecording(
+ currentChannel.getId());
+ if (currentRecording != null) {
+ mDvrManager.stopRecording(currentRecording);
+ }
+ }
+ }
+ });
+ }
+ }
+ return true;
+ }
}
}
+ if (keyCode == KeyEvent.KEYCODE_WINDOW) {
+ // Consumes the PIP button to prevent entering PIP mode
+ // in case that TV isn't showing properly (e.g. no browsable channel)
+ return true;
+ }
if (SystemProperties.USE_DEBUG_KEYS.getValue() || BuildConfig.ENG) {
switch (keyCode) {
case KeyEvent.KEYCODE_W: {
@@ -2578,36 +2656,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mOverlayManager.getSideFragmentManager().show(new DisplayModeFragment());
return true;
}
-
case KeyEvent.KEYCODE_D:
mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment());
return true;
-
- case KeyEvent.KEYCODE_MEDIA_RECORD: // TODO(DVR) handle with debug_keys set
- case KeyEvent.KEYCODE_V: {
- DvrManager dvrManager = TvApplication.getSingletons(this).getDvrManager();
- long startTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5);
- long endTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35);
- dvrManager.addSchedule(getCurrentChannel(), startTime, endTime);
- return true;
- }
- case KeyEvent.KEYCODE_PROG_BLUE:
- case KeyEvent.KEYCODE_BUTTON_X:
- case KeyEvent.KEYCODE_X:
- if (CommonFeatures.DVR.isEnabled(this)) {
- Channel channel = mTvView.getCurrentChannel();
- long channelId = channel.getId();
- Program p = mProgramDataManager.getCurrentProgram(channelId);
- if (p == null) {
- long now = System.currentTimeMillis();
- mDvrManager
- .addSchedule(channel, now, now + TimeUnit.MINUTES.toMillis(1));
- } else {
- mDvrManager.addSchedule(p, mDvrManager.getConflictingSchedules(p));
- }
- return true;
- }
- break;
}
}
return super.onKeyUp(keyCode, event);
@@ -3109,8 +3160,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
- initAnimations();
- initSideFragments();
+ if (mActivityStarted) {
+ initAnimations();
+ initSideFragments();
+ }
}
}, LAZY_INITIALIZATION_DELAY);
}
@@ -3142,13 +3195,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
switch (msg.what) {
case MSG_CHANNEL_DOWN_PRESSED:
long startTime = (Long) msg.obj;
- mainActivity.moveToAdjacentChannel(false, true);
+ // message re-sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
+ mainActivity.moveToAdjacentChannel(false, true);
break;
case MSG_CHANNEL_UP_PRESSED:
startTime = (Long) msg.obj;
- mainActivity.moveToAdjacentChannel(true, true);
+ // message re-sending should be done before moving channel, because we use the
+ // existence of message to decide if users are switching channel.
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
+ mainActivity.moveToAdjacentChannel(true, true);
break;
case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE:
mainActivity.updateChannelBannerAndShowIfNeeded(
@@ -3262,7 +3319,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
}
-
+ mChannelBannerView.setBlockingContentRating(rating);
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
mTvViewUiManager.fadeInTvView();
}
@@ -3272,6 +3329,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!isUnderShrunkenTvView()) {
mUnlockAllowedRatingBeforeShrunken = false;
}
+ mChannelBannerView.setBlockingContentRating(null);
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
}
}
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index 650f5191..2d6d45c4 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -19,8 +19,6 @@ package com.android.tv;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context;
-import android.os.AsyncTask;
-import android.os.AsyncTask.Status;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.IntDef;
@@ -63,8 +61,7 @@ import java.util.concurrent.TimeUnit;
*/
public class TimeShiftManager {
private static final String TAG = "TimeShiftManager";
- // STOPSHIP: Turn this flag off once b/31074952 is fixed.
- private static final boolean DEBUG = true;
+ private static final boolean DEBUG = false;
@Retention(RetentionPolicy.SOURCE)
@IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING})
@@ -894,7 +891,6 @@ public class TimeShiftManager {
endTimeMs + PREFETCH_DURATION_FOR_NEXT);
if (needToLoad) {
Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs);
- SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available");
mProgramLoadQueue.add(period);
startTaskIfNeeded();
}
@@ -907,52 +903,24 @@ public class TimeShiftManager {
if (mProgramLoadTask == null || mProgramLoadTask.isCancelled()) {
startNext();
} else {
- switch (mProgramLoadTask.getStatus()) {
- case PENDING:
- SoftPreconditions.checkState(false, TAG,
- "The state of task can not be PENDING");
- if (mProgramLoadTask.overlaps(mProgramLoadQueue)) {
- if (mProgramLoadTask.cancel(true)) {
- SoftPreconditions.checkState(isAvailable(), TAG,
- "Time shifting is not available");
- mProgramLoadQueue.add(mProgramLoadTask.getPeriod());
- mProgramLoadTask = null;
- startNext();
- }
- }
- break;
- case RUNNING:
- // Remove pending task fully satisfied by the current
- Range<Long> current = mProgramLoadTask.getPeriod();
- Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
- while (i.hasNext()) {
- Range<Long> r = i.next();
- if (current.contains(r)) {
- i.remove();
- }
- }
- break;
- case FINISHED:
- SoftPreconditions.checkState(false, TAG,
- "The state of task can not be FINISHED");
- // The task should have already cleared it self, clear and restart anyways.
- Log.w(TAG, mProgramLoadTask + " is finished, but was not cleared");
- startNext();
- break;
+ // Remove pending task fully satisfied by the current
+ Range<Long> current = mProgramLoadTask.getPeriod();
+ Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
+ while (i.hasNext()) {
+ Range<Long> r = i.next();
+ if (current.contains(r)) {
+ i.remove();
+ }
}
}
}
private void startNext() {
- SoftPreconditions.checkState(mProgramLoadTask == null
- || mProgramLoadTask.getStatus() == Status.RUNNING, TAG,
- "The status of task should be \"RUNNING\"");
mProgramLoadTask = null;
if (mProgramLoadQueue.isEmpty()) {
return;
}
- SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting should be available.");
Range<Long> next = mProgramLoadQueue.poll();
// Extend next to include any overlapping Ranges.
Iterator<Range<Long>> i = mProgramLoadQueue.iterator();
@@ -1134,7 +1102,6 @@ public class TimeShiftManager {
private void schedulePrefetchPrograms() {
if (DEBUG) Log.d(TAG, "Scheduling prefetching programs.");
- SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available");
if (mHandler.hasMessages(MSG_PREFETCH_PROGRAM)) {
return;
}
@@ -1172,7 +1139,6 @@ public class TimeShiftManager {
// Prefetch programs within PREFETCH_DURATION_FOR_NEXT from now.
private void prefetchPrograms() {
- SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available");
long startTimeMs;
Program lastValidProgram = getLastValidProgram();
if (lastValidProgram == null) {
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index e15bad02..0e18a259 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -55,6 +55,7 @@ import com.android.tv.dvr.DvrDataManagerImpl;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrRecordingService;
import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.tvinput.TunerTvInputService;
@@ -95,6 +96,7 @@ public class TvApplication extends Application implements ApplicationSingletons
private DvrManager mDvrManager;
private DvrScheduleManager mDvrScheduleManager;
private DvrDataManager mDvrDataManager;
+ private DvrStorageStatusManager mDvrStorageStatusManager;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
@Nullable
private InputSessionManager mInputSessionManager;
@@ -129,14 +131,16 @@ public class TvApplication extends Application implements ApplicationSingletons
// Only set StrictMode for ENG builds because the build server only produces userdebug
// builds.
if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
- StrictMode.setThreadPolicy(
- new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
- StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder()
- .detectAll().penaltyLog();
- if (BuildConfig.ENG && SystemProperties.ALLOW_DEATH_PENALTY.getValue() &&
- !TvCommonUtils.isRunningInTest()) {
- // TODO turn on death penalty for tests when they stop leaking MainActivity
+ StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
+ new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
+ StrictMode.VmPolicy.Builder vmPolicyBuilder =
+ new StrictMode.VmPolicy.Builder().detectAll().penaltyLog();
+ if (!TvCommonUtils.isRunningInTest()) {
+ threadPolicyBuilder.penaltyDialog();
+ // Turn off death penalty for tests b/23355898
+ vmPolicyBuilder.penaltyDeath();
}
+ StrictMode.setThreadPolicy(threadPolicyBuilder.build());
StrictMode.setVmPolicy(vmPolicyBuilder.build());
}
if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) {
@@ -160,6 +164,9 @@ public class TvApplication extends Application implements ApplicationSingletons
return;
}
mRunningInMainProcess = isMainProcess;
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess);
+ }
if (mRunningInMainProcess) {
mTvInputManagerHelper.addCallback(new TvInputCallback() {
@Override
@@ -286,14 +293,23 @@ public class TvApplication extends Application implements ApplicationSingletons
@Override
public DvrDataManager getDvrDataManager() {
if (mDvrDataManager == null) {
- DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM);
- mDvrDataManager = dvrDataManager;
- dvrDataManager.start();
+ DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM);
+ mDvrDataManager = dvrDataManager;
+ dvrDataManager.start();
}
return mDvrDataManager;
}
/**
+ * Returns {@link DvrStorageStatusManager}.
+ */
+ @TargetApi(Build.VERSION_CODES.N)
+ @Override
+ public DvrStorageStatusManager getDvrStorageStatusManager() {
+ return mDvrStorageStatusManager;
+ }
+
+ /**
* Returns {@link TvInputManagerHelper}.
*/
@Override
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index c32da431..f420de02 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -129,6 +129,11 @@ public abstract class BaseProgram {
abstract public long getDurationMillis();
/**
+ * Returns the series ID.
+ */
+ abstract public String getSeriesId();
+
+ /**
* Returns the season number.
*/
abstract public String getSeasonNumber();
@@ -149,6 +154,11 @@ public abstract class BaseProgram {
abstract public String getThumbnailUri();
/**
+ * Returns the array of the ID's of the canonical genres.
+ */
+ abstract public int[] getCanonicalGenreIds();
+
+ /**
* Returns channel's ID of the program.
*/
abstract public long getChannelId();
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 898324f0..30f84236 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -150,11 +150,6 @@ public final class Channel {
private long mDvrId;
- /**
- * TODO(DVR): Need to fill the following data.
- */
- private boolean mRecordable;
-
private Channel() {
// Do nothing.
}
@@ -566,6 +561,8 @@ public final class Channel {
getUri().toString());
mAppLinkType = APP_LINK_TYPE_CHANNEL;
return;
+ } else {
+ Log.w(TAG, "No activity exists to handle : " + mAppLinkIntentUri);
}
} catch (URISyntaxException e) {
Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e);
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index af7ed904..6f9ea6d7 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -297,6 +297,16 @@ public class ChannelDataManager {
}
/**
+ * Checks if the channel exists in DB.
+ *
+ * <p>Note that the channels of the removed inputs can not be obtained from {@link #getChannel}.
+ * In that case this method is used to check if the channel exists in the DB.
+ */
+ public boolean doesChannelExistInDb(long channelId) {
+ return mChannelWrapperMap.get(channelId) != null;
+ }
+
+ /**
* Returns true if and only if there exists at least one channel and all channels are hidden.
*/
public boolean areAllChannelsHidden() {
diff --git a/src/com/android/tv/data/Lineup.java b/src/com/android/tv/data/Lineup.java
index 3e162d1c..d0e9d7ba 100644
--- a/src/com/android/tv/data/Lineup.java
+++ b/src/com/android/tv/data/Lineup.java
@@ -46,7 +46,6 @@ public class Lineup {
*/
public final String location;
- /** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef({LINEUP_CABLE, LINEUP_SATELLITE, LINEUP_BROADCAST_DIGITAL, LINEUP_BROADCAST_ANALOG,
LINEUP_IPTV, LINEUP_MVPD})
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index 05d7d9c4..b9cd3d8d 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -260,6 +260,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
/**
* Returns the series ID.
*/
+ @Override
public String getSeriesId() {
return mSeriesId;
}
@@ -400,6 +401,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
/**
* Returns array of canonical genre ID's for this program.
*/
+ @Override
public int[] getCanonicalGenreIds() {
return mCanonicalGenreIds;
}
diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java
index df842737..fe461f14 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -16,6 +16,8 @@
package com.android.tv.data;
+import android.media.tv.TvContentRating;
+
public interface StreamInfo {
int VIDEO_DEFINITION_LEVEL_UNKNOWN = 0;
int VIDEO_DEFINITION_LEVEL_SD = 1;
@@ -26,6 +28,7 @@ public interface StreamInfo {
int AUDIO_CHANNEL_COUNT_UNKNOWN = 0;
Channel getCurrentChannel();
+ TvContentRating getBlockedContentRating();
int getVideoWidth();
int getVideoHeight();
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index dd46caf1..3b093b6a 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -30,7 +30,6 @@ import android.media.tv.TvContract;
import android.media.tv.TvContract.Programs;
import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
-import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
@@ -45,13 +44,11 @@ import android.util.Log;
import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
-import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.InternalDataUtils;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesRecordingScheduler;
import com.android.tv.util.LocationUtils;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.Utils;
@@ -69,13 +66,13 @@ import java.util.concurrent.TimeUnit;
*/
public class EpgFetcher {
private static final String TAG = "EpgFetcher";
- private static final boolean DEBUG = true; // STOPSHIP(DVR)
+ private static final boolean DEBUG = false;
private static final int MSG_FETCH_EPG = 1;
private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4);
private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1);
- private static final long LOCATION_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1);
+ private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1);
private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
@@ -184,17 +181,16 @@ public class EpgFetcher {
try {
Address address = LocationUtils.getCurrentAddress(mContext);
- if (address == null) {
- if (DEBUG) Log.d(TAG, "Failed to get the current address.");
- return false;
- }
- if (!TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
+ if (address != null
+ && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode());
return false;
}
- } catch (IOException | SecurityException e) {
- Log.w(TAG, "Exception when getting the current location", e);
+ } catch (SecurityException e) {
+ Log.w(TAG, "No permission to get the current location", e);
return false;
+ } catch (IOException e) {
+ Log.w(TAG, "IO Exception when getting the current location", e);
}
return true;
}
@@ -319,15 +315,6 @@ public class EpgFetcher {
final boolean epgUpdated = updated;
setLastUpdatedEpgTimestamp(epgTimestamp);
mHandler.removeMessages(MSG_FETCH_EPG);
- new Handler(Looper.getMainLooper()).post(new Runnable() {
- @Override
- public void run() {
- mRecurringRunner.resetNextRunTime();
- if (epgUpdated && CommonFeatures.DVR.isEnabled(mContext)) {
- SeriesRecordingScheduler.getInstance(mContext).updateSchedules();
- }
- }
- });
if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
}
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 6af77940..89661df3 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -30,9 +30,10 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState;
import com.android.tv.util.Clock;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -136,35 +137,35 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
/**
- * Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)}
+ * Calls {@link RecordedProgramListener#onRecordedProgramsAdded}
* for each listener.
*/
- protected final void notifyRecordedProgramAdded(RecordedProgram recordedProgram) {
+ protected final void notifyRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
for (RecordedProgramListener l : mRecordedProgramListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "added " + recordedProgram);
- l.onRecordedProgramAdded(recordedProgram);
+ if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(recordedPrograms));
+ l.onRecordedProgramsAdded(recordedPrograms);
}
}
/**
- * Calls {@link RecordedProgramListener#onRecordedProgramChanged(RecordedProgram)}
+ * Calls {@link RecordedProgramListener#onRecordedProgramsChanged}
* for each listener.
*/
- protected final void notifyRecordedProgramChanged(RecordedProgram recordedProgram) {
+ protected final void notifyRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
for (RecordedProgramListener l : mRecordedProgramListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "changed " + recordedProgram);
- l.onRecordedProgramChanged(recordedProgram);
+ if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(recordedPrograms));
+ l.onRecordedProgramsChanged(recordedPrograms);
}
}
/**
- * Calls {@link RecordedProgramListener#onRecordedProgramRemoved(RecordedProgram)}
+ * Calls {@link RecordedProgramListener#onRecordedProgramsRemoved}
* for each listener.
*/
- protected final void notifyRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ protected final void notifyRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
for (RecordedProgramListener l : mRecordedProgramListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "removed " + recordedProgram);
- l.onRecordedProgramRemoved(recordedProgram);
+ if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(recordedPrograms));
+ l.onRecordedProgramsRemoved(recordedPrograms);
}
}
@@ -174,7 +175,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
*/
protected final void notifySeriesRecordingAdded(SeriesRecording... seriesRecordings) {
for (SeriesRecordingListener l : mSeriesRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "added " + seriesRecordings);
+ if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(seriesRecordings));
l.onSeriesRecordingAdded(seriesRecordings);
}
}
@@ -185,7 +186,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
*/
protected final void notifySeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
for (SeriesRecordingListener l : mSeriesRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "removed " + seriesRecordings);
+ if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(seriesRecordings));
l.onSeriesRecordingRemoved(seriesRecordings);
}
}
@@ -197,7 +198,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
*/
protected final void notifySeriesRecordingChanged(SeriesRecording... seriesRecordings) {
for (SeriesRecordingListener l : mSeriesRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "changed " + seriesRecordings);
+ if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(seriesRecordings));
l.onSeriesRecordingChanged(seriesRecordings);
}
}
@@ -208,7 +209,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
*/
protected final void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecording) {
for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording);
+ if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(scheduledRecording));
l.onScheduledRecordingAdded(scheduledRecording);
}
}
@@ -219,7 +220,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
*/
protected final void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecording) {
for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "removed " + scheduledRecording);
+ if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(scheduledRecording));
l.onScheduledRecordingRemoved(scheduledRecording);
}
}
@@ -232,7 +233,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
protected final void notifyScheduledRecordingStatusChanged(
ScheduledRecording... scheduledRecording) {
for (ScheduledRecordingListener l : mScheduledRecordingListeners) {
- if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording);
+ if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(scheduledRecording));
l.onScheduledRecordingStatusChanged(scheduledRecording);
}
}
@@ -259,14 +260,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
- public List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings() {
- return filterEndTimeIsPast(getRecordingsWithState(
- ScheduledRecording.STATE_RECORDING_IN_PROGRESS,
- ScheduledRecording.STATE_RECORDING_NOT_STARTED,
- ScheduledRecording.STATE_RECORDING_CANCELED));
- }
-
- @Override
public List<ScheduledRecording> getStartedRecordings() {
return filterEndTimeIsPast(getRecordingsWithState(
ScheduledRecording.STATE_RECORDING_IN_PROGRESS));
@@ -274,8 +267,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
@Override
public List<ScheduledRecording> getNonStartedScheduledRecordings() {
- Set<Integer> states = new HashSet<>();
- states.add(ScheduledRecording.STATE_RECORDING_NOT_STARTED);
return filterEndTimeIsPast(getRecordingsWithState(
ScheduledRecording.STATE_RECORDING_NOT_STARTED));
}
@@ -314,6 +305,9 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
@Override
public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) {
SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId);
+ if (seriesRecording == null) {
+ return Collections.emptyList();
+ }
List<RecordedProgram> result = new ArrayList<>();
for (RecordedProgram r : getRecordedPrograms()) {
if (seriesRecording.getSeriesId().equals(r.getSeriesId())) {
@@ -322,4 +316,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
return result;
}
+
+ @Override
+ public void forgetStorage(String inputId) { }
}
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index 126f3e74..06613667 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -69,11 +69,6 @@ public interface DvrDataManager {
List<ScheduledRecording> getAvailableScheduledRecordings();
/**
- * Return all available and canceled {@link ScheduledRecording}.
- */
- List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings();
-
- /**
* Returns started recordings that expired.
*/
List<ScheduledRecording> getStartedRecordings();
@@ -260,10 +255,10 @@ public interface DvrDataManager {
* Listens for changes to {@link RecordedProgram}s.
*/
interface RecordedProgramListener {
- void onRecordedProgramAdded(RecordedProgram recordedProgram);
+ void onRecordedProgramsAdded(RecordedProgram... recordedPrograms);
- void onRecordedProgramChanged(RecordedProgram recordedProgram);
+ void onRecordedProgramsChanged(RecordedProgram... recordedPrograms);
- void onRecordedProgramRemoved(RecordedProgram recordedProgram);
+ void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms);
}
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 5ae2c4ea..46682a48 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -16,12 +16,16 @@
package com.android.tv.dvr;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
+import android.database.sqlite.SQLiteException;
import android.media.tv.TvContract.RecordedPrograms;
+import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputManager.TvInputCallback;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -37,7 +41,7 @@ import android.util.Range;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener;
import com.android.tv.dvr.ScheduledRecording.RecordingState;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
@@ -47,9 +51,13 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask;
+import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
import com.android.tv.util.Clock;
+import com.android.tv.util.Filter;
+import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvProviderUriMatcher;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -69,6 +77,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private static final String TAG = "DvrDataManagerImpl";
private static final boolean DEBUG = false;
+ private final TvInputManagerHelper mInputManager;
+
private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>();
@@ -76,6 +86,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
new HashMap<>();
private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>();
+ private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput =
+ new HashMap<>();
+ private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>();
+ private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>();
+
private final Context mContext;
private final ContentObserver mContentObserver = new ContentObserver(new Handler(
Looper.getMainLooper())) {
@@ -96,15 +111,68 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
- private final DvrDbSync mDbSync;
+ private DvrDbSync mDbSync;
+ private DvrStorageStatusManager mStorageStatusManager;
+
+ private final TvInputCallback mInputCallback = new TvInputCallback() {
+ @Override
+ public void onInputAdded(String inputId) {
+ if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
+ if (!isInputAvailable(inputId)) {
+ if (DEBUG) Log.d(TAG, "Not available for recording");
+ return;
+ }
+ unhideInput(inputId);
+ }
+
+ @Override
+ public void onInputRemoved(String inputId) {
+ if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
+ hideInput(inputId);
+ }
+ };
+
+ private final OnStorageMountChangedListener mStorageMountChangedListener =
+ new OnStorageMountChangedListener() {
+ @Override
+ public void onStorageMountChanged(boolean storageMounted) {
+ for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) {
+ if (Utils.isBundledInput(input.getId())) {
+ if (storageMounted) {
+ unhideInput(input.getId());
+ } else {
+ hideInput(input.getId());
+ }
+ }
+ }
+ }
+ };
+
+ private static <T> List<T> moveElements(HashMap<Long, T> from, HashMap<Long, T> to,
+ Filter<T> filter) {
+ List<T> moved = new ArrayList<>();
+ Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
+ while (iter.hasNext()) {
+ Entry<Long, T> entry = iter.next();
+ if (filter.filter(entry.getValue())) {
+ to.put(entry.getKey(), entry.getValue());
+ iter.remove();
+ moved.add(entry.getValue());
+ }
+ }
+ return moved;
+ }
public DvrDataManagerImpl(Context context, Clock clock) {
super(context, clock);
mContext = context;
- mDbSync = new DvrDbSync(context, this);
+ mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper();
+ mStorageStatusManager = TvApplication.getSingletons(context).getDvrStorageStatusManager();
}
public void start() {
+ mInputManager.addCallback(mInputCallback);
+ mStorageStatusManager.addListener(mStorageMountChangedListener);
AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask
= new AsyncDvrQuerySeriesRecordingTask(mContext) {
@Override
@@ -116,9 +184,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
mPendingTasks.remove(this);
long maxId = 0;
+ HashSet<String> seriesIds = new HashSet<>();
for (SeriesRecording r : seriesRecordings) {
- mSeriesRecordings.put(r.getId(), r);
- mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ if (SoftPreconditions.checkState(!seriesIds.contains(r.getSeriesId()), TAG,
+ "Skip loading series recording with duplicate series ID: " + r)) {
+ seriesIds.add(r.getSeriesId());
+ if (isInputAvailable(r.getInputId())) {
+ mSeriesRecordings.put(r.getId(), r);
+ mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ } else {
+ mSeriesRecordingsForRemovedInput.put(r.getId(), r);
+ }
+ }
if (maxId < r.getId()) {
maxId = r.getId();
}
@@ -128,20 +205,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
};
dvrQuerySeriesRecordingTask.executeOnDbThread();
mPendingTasks.add(dvrQuerySeriesRecordingTask);
- AsyncDvrQueryScheduleTask dvrQueryRecordingTask
+ AsyncDvrQueryScheduleTask dvrQueryScheduleTask
= new AsyncDvrQueryScheduleTask(mContext) {
@Override
protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
mPendingTasks.remove(this);
}
+ @SuppressLint("SwitchIntDef")
@Override
protected void onPostExecute(List<ScheduledRecording> result) {
mPendingTasks.remove(this);
long maxId = 0;
+ List<SeriesRecording> seriesRecordingsToAdd = new ArrayList<>();
List<ScheduledRecording> toUpdate = new ArrayList<>();
+ List<ScheduledRecording> toDelete = new ArrayList<>();
for (ScheduledRecording r : result) {
- if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
+ if (!isInputAvailable(r.getInputId())) {
+ mScheduledRecordingsForRemovedInput.put(r.getId(), r);
+ } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
getDeletedScheduleMap().put(r.getProgramId(), r);
} else {
mScheduledRecordings.put(r.getId(), r);
@@ -149,22 +231,29 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
// Adjust the state of the schedules before DB loading is finished.
- if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- toUpdate.add(ScheduledRecording.buildFrom(r)
- .setState(ScheduledRecording.STATE_RECORDING_FAILED)
- .build());
- } else {
- toUpdate.add(ScheduledRecording.buildFrom(r)
- .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
- .build());
- }
- } else if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
- if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
- toUpdate.add(ScheduledRecording.buildFrom(r)
- .setState(ScheduledRecording.STATE_RECORDING_FAILED)
- .build());
- }
+ switch (r.getState()) {
+ case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
+ if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
+ toUpdate.add(ScheduledRecording.buildFrom(r)
+ .setState(ScheduledRecording.STATE_RECORDING_FAILED)
+ .build());
+ } else {
+ toUpdate.add(ScheduledRecording.buildFrom(r)
+ .setState(
+ ScheduledRecording.STATE_RECORDING_NOT_STARTED)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
+ if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
+ toUpdate.add(ScheduledRecording.buildFrom(r)
+ .setState(ScheduledRecording.STATE_RECORDING_FAILED)
+ .build());
+ }
+ break;
+ case ScheduledRecording.STATE_RECORDING_CANCELED:
+ toDelete.add(r);
+ break;
}
}
if (maxId < r.getId()) {
@@ -172,19 +261,23 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (!toUpdate.isEmpty()) {
- updateScheduledRecording(true, ScheduledRecording.toArray(toUpdate));
+ updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
+ }
+ if (!toDelete.isEmpty()) {
+ removeScheduledRecording(ScheduledRecording.toArray(toDelete));
}
IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
mDvrLoadFinished = true;
notifyDvrScheduleLoadFinished();
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
mDbSync.start();
if (isInitialized()) {
SeriesRecordingScheduler.getInstance(mContext).start();
}
}
};
- dvrQueryRecordingTask.executeOnDbThread();
- mPendingTasks.add(dvrQueryRecordingTask);
+ dvrQueryScheduleTask.executeOnDbThread();
+ mPendingTasks.add(dvrQueryScheduleTask);
RecordedProgramsQueryTask mRecordedProgramQueryTask =
new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
mRecordedProgramQueryTask.executeOnDbThread();
@@ -193,8 +286,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
public void stop() {
+ mInputManager.removeCallback(mInputCallback);
+ mStorageStatusManager.removeListener(mStorageMountChangedListener);
SeriesRecordingScheduler.getInstance(mContext).stop();
- mDbSync.stop();
+ if (mDbSync != null) {
+ mDbSync.stop();
+ }
ContentResolver cr = mContext.getContentResolver();
cr.unregisterContentObserver(mContentObserver);
Iterator<AsyncTask> i = mPendingTasks.iterator();
@@ -213,52 +310,76 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) {
if (!mRecordedProgramLoadFinished) {
for (RecordedProgram recorded : recordedPrograms) {
- mRecordedPrograms.put(recorded.getId(), recorded);
+ if (isInputAvailable(recorded.getInputId())) {
+ mRecordedPrograms.put(recorded.getId(), recorded);
+ } else {
+ mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
+ }
}
mRecordedProgramLoadFinished = true;
notifyRecordedProgramLoadFinished();
} else if (recordedPrograms == null || recordedPrograms.isEmpty()) {
- for (RecordedProgram recorded : mRecordedPrograms.values()) {
- notifyRecordedProgramRemoved(recorded);
- }
+ List<RecordedProgram> oldRecordedPrograms =
+ new ArrayList<>(mRecordedPrograms.values());
mRecordedPrograms.clear();
+ mRecordedProgramsForRemovedInput.clear();
+ notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms));
} else {
HashMap<Long, RecordedProgram> oldRecordedPrograms
= new HashMap<>(mRecordedPrograms);
mRecordedPrograms.clear();
+ mRecordedProgramsForRemovedInput.clear();
+ List<RecordedProgram> addedRecordedPrograms = new ArrayList<>();
+ List<RecordedProgram> changedRecordedPrograms = new ArrayList<>();
for (RecordedProgram recorded : recordedPrograms) {
- mRecordedPrograms.put(recorded.getId(), recorded);
- RecordedProgram old = oldRecordedPrograms.remove(recorded.getId());
- if (old == null) {
- notifyRecordedProgramAdded(recorded);
+ if (isInputAvailable(recorded.getInputId())) {
+ mRecordedPrograms.put(recorded.getId(), recorded);
+ if (oldRecordedPrograms.remove(recorded.getId()) == null) {
+ addedRecordedPrograms.add(recorded);
+ } else {
+ changedRecordedPrograms.add(recorded);
+ }
} else {
- notifyRecordedProgramChanged(recorded);
+ mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
}
}
- for (RecordedProgram recorded : oldRecordedPrograms.values()) {
- notifyRecordedProgramRemoved(recorded);
+ if (!addedRecordedPrograms.isEmpty()) {
+ notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms));
+ }
+ if (!changedRecordedPrograms.isEmpty()) {
+ notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms));
+ }
+ if (!oldRecordedPrograms.isEmpty()) {
+ notifyRecordedProgramsRemoved(
+ RecordedProgram.toArray(oldRecordedPrograms.values()));
}
}
if (isInitialized()) {
SeriesRecordingScheduler.getInstance(mContext).start();
}
} else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
+ if (!mRecordedProgramLoadFinished) {
+ return;
+ }
long id = ContentUris.parseId(uri);
if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ mRecordedProgramsForRemovedInput.remove(id);
RecordedProgram old = mRecordedPrograms.remove(id);
if (old != null) {
- notifyRecordedProgramRemoved(old);
- } else {
- Log.w(TAG, "Could not find old version of deleted program #" + id);
+ notifyRecordedProgramsRemoved(old);
}
} else {
- RecordedProgram newRecorded = recordedPrograms.get(0);
- RecordedProgram old = mRecordedPrograms.put(id, newRecorded);
- if (old == null) {
- notifyRecordedProgramAdded(newRecorded);
+ RecordedProgram recordedProgram = recordedPrograms.get(0);
+ if (isInputAvailable(recordedProgram.getInputId())) {
+ RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
+ if (old == null) {
+ notifyRecordedProgramsAdded(recordedProgram);
+ } else {
+ notifyRecordedProgramsChanged(recordedProgram);
+ }
} else {
- notifyRecordedProgramChanged(newRecorded);
+ mRecordedProgramsForRemovedInput.put(id, recordedProgram);
}
}
}
@@ -432,7 +553,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
@Override
public void addScheduledRecording(ScheduledRecording... schedules) {
for (ScheduledRecording r : schedules) {
- r.setId(IdGenerator.SCHEDULED_RECORDING.newId());
+ if (r.getId() == ScheduledRecording.ID_NOT_SET) {
+ r.setId(IdGenerator.SCHEDULED_RECORDING.newId());
+ }
mScheduledRecordings.put(r.getId(), r);
if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
@@ -450,7 +573,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
for (SeriesRecording r : seriesRecordings) {
r.setId(IdGenerator.SERIES_RECORDING.newId());
mSeriesRecordings.put(r.getId(), r);
- mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ SoftPreconditions.checkArgument(previousSeries == null, TAG, "Attempt to add series"
+ + " recording with the duplicate series ID: " + r.getSeriesId());
}
if (mDvrLoadFinished) {
notifySeriesRecordingAdded(seriesRecordings);
@@ -463,18 +588,22 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
removeScheduledRecording(false, schedules);
}
- private void removeScheduledRecording(boolean forceDelete, ScheduledRecording... schedules) {
+ @Override
+ public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>();
for (ScheduledRecording r : schedules) {
mScheduledRecordings.remove(r.getId());
- if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
- mProgramId2ScheduledRecordings.remove(r.getProgramId());
- }
- // If it belongs to the series recording and it's not started yet, do not delete.
- // Instead mark deleted.
- if (!forceDelete && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
- && r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ getDeletedScheduleMap().remove(r.getId());
+ mProgramId2ScheduledRecordings.remove(r.getProgramId());
+ boolean isScheduleForRemovedInput =
+ mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null;
+ // If it belongs to the series recording and it's not started yet, just mark delete
+ // instead of deleting it.
+ if (!isScheduleForRemovedInput && !forceRemove
+ && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
+ && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) {
SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET);
ScheduledRecording deleted = ScheduledRecording.buildFrom(r)
.setState(ScheduledRecording.STATE_RECORDING_DELETED).build();
@@ -538,8 +667,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
updateScheduledRecording(true, schedules);
}
- private void updateScheduledRecording(boolean updateDb,
- final ScheduledRecording... schedules) {
+ private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) {
List<ScheduledRecording> toUpdate = new ArrayList<>();
for (ScheduledRecording r : schedules) {
if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG,
@@ -550,9 +678,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r);
// The channel ID should not be changed.
SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId());
- if (DEBUG) Log.d(TAG, "Updating " + oldScheduledRecording + " with " + r);
long programId = r.getProgramId();
- if (oldScheduledRecording != null && oldScheduledRecording.getProgramId() != programId
+ if (oldScheduledRecording.getProgramId() != programId
&& oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
.get(oldScheduledRecording.getProgramId());
@@ -565,6 +692,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mProgramId2ScheduledRecordings.put(programId, r);
}
}
+ if (toUpdate.isEmpty()) {
+ return;
+ }
ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate);
if (mDvrLoadFinished) {
notifyScheduledRecordingStatusChanged(scheduleArray);
@@ -572,17 +702,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (updateDb) {
new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
}
- removeDeletedSchedules(scheduleArray);
+ removeDeletedSchedules(schedules);
}
@Override
public void updateSeriesRecording(final SeriesRecording... seriesRecordings) {
for (SeriesRecording r : seriesRecordings) {
- SeriesRecording old = mSeriesRecordings.put(r.getId(), r);
- if (old != null) {
- mSeriesId2SeriesRecordings.remove(old.getSeriesId());
- }
- mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r);
+ SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
+ SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be"
+ + " updated: " + r);
}
if (mDvrLoadFinished) {
notifySeriesRecordingChanged(seriesRecordings);
@@ -590,6 +719,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
}
+ private boolean isInputAvailable(String inputId) {
+ return mInputManager.hasTvInputInfo(inputId)
+ && (!Utils.isBundledInput(inputId) || mStorageStatusManager.isStorageMounted());
+ }
+
private void removeDeletedSchedules(ScheduledRecording... addedSchedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
for (ScheduledRecording r : addedSchedules) {
@@ -625,6 +759,148 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
+ private void unhideInput(String inputId) {
+ if (DEBUG) Log.d(TAG, "unhideInput " + inputId);
+ List<ScheduledRecording> movedSchedules =
+ moveElements(mScheduledRecordingsForRemovedInput, mScheduledRecordings,
+ new Filter<ScheduledRecording>() {
+ @Override
+ public boolean filter(ScheduledRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ List<SeriesRecording> movedSeriesRecordings =
+ moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings,
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ List<RecordedProgram> movedRecordedPrograms =
+ moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms,
+ new Filter<RecordedProgram>() {
+ @Override
+ public boolean filter(RecordedProgram r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ if (!movedSchedules.isEmpty()) {
+ for (ScheduledRecording schedule : movedSchedules) {
+ mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule);
+ }
+ }
+ if (!movedSeriesRecordings.isEmpty()) {
+ for (SeriesRecording seriesRecording : movedSeriesRecordings) {
+ mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording);
+ }
+ }
+ // Notify after all the data are moved.
+ if (!movedSchedules.isEmpty()) {
+ notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
+ }
+ if (!movedSeriesRecordings.isEmpty()) {
+ notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings));
+ }
+ if (!movedRecordedPrograms.isEmpty()) {
+ notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms));
+ }
+ }
+
+ private void hideInput(String inputId) {
+ if (DEBUG) Log.d(TAG, "hideInput " + inputId);
+ List<ScheduledRecording> movedSchedules =
+ moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput,
+ new Filter<ScheduledRecording>() {
+ @Override
+ public boolean filter(ScheduledRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ List<SeriesRecording> movedSeriesRecordings =
+ moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput,
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ List<RecordedProgram> movedRecordedPrograms =
+ moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput,
+ new Filter<RecordedProgram>() {
+ @Override
+ public boolean filter(RecordedProgram r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
+ if (!movedSchedules.isEmpty()) {
+ for (ScheduledRecording schedule : movedSchedules) {
+ mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
+ }
+ }
+ if (!movedSeriesRecordings.isEmpty()) {
+ for (SeriesRecording seriesRecording : movedSeriesRecordings) {
+ mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId());
+ }
+ }
+ // Notify after all the data are moved.
+ if (!movedSchedules.isEmpty()) {
+ notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules));
+ }
+ if (!movedSeriesRecordings.isEmpty()) {
+ notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings));
+ }
+ if (!movedRecordedPrograms.isEmpty()) {
+ notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms));
+ }
+ }
+
+ @Override
+ public void forgetStorage(String inputId) {
+ List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
+ for (Iterator<ScheduledRecording> i =
+ mScheduledRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) {
+ ScheduledRecording r = i.next();
+ if (inputId.equals(r.getInputId())) {
+ schedulesToDelete.add(r);
+ i.remove();
+ }
+ }
+ List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>();
+ for (Iterator<SeriesRecording> i =
+ mSeriesRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) {
+ SeriesRecording r = i.next();
+ if (inputId.equals(r.getInputId())) {
+ seriesRecordingsToDelete.add(r);
+ i.remove();
+ }
+ }
+ for (Iterator<RecordedProgram> i =
+ mRecordedProgramsForRemovedInput.values().iterator(); i.hasNext(); ) {
+ if (inputId.equals(i.next().getInputId())) {
+ i.remove();
+ }
+ }
+ new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
+ ScheduledRecording.toArray(schedulesToDelete));
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(
+ SeriesRecording.toArray(seriesRecordingsToDelete));
+ new AsyncDbTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContentResolver resolver = mContext.getContentResolver();
+ String args[] = { inputId };
+ try {
+ resolver.delete(RecordedPrograms.CONTENT_URI,
+ RecordedPrograms.COLUMN_INPUT_ID + " = ?", args);
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e);
+ }
+ return null;
+ }
+ }.executeOnDbThread();
+ }
+
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java
index baa7f3d9..df181455 100644
--- a/src/com/android/tv/dvr/DvrDbSync.java
+++ b/src/com/android/tv/dvr/DvrDbSync.java
@@ -28,73 +28,132 @@ import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import com.android.tv.TvApplication;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
import com.android.tv.util.TvProviderUriMatcher;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedList;
+import java.util.List;
import java.util.Objects;
import java.util.Queue;
+import java.util.Set;
/**
* A class to synchronizes DVR DB with TvProvider.
+ *
+ * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the
+ * other tasks are blocked until the current one finishes. As this class performs the low priority
+ * jobs which take long time, it should not block others if possible. For this reason, only one
+ * program is queried at a time and others are queued and will be executed on the other
+ * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask.
*/
@MainThread
@TargetApi(Build.VERSION_CODES.N)
class DvrDbSync {
+ private static final String TAG = "DvrDbSync";
+ private static final boolean DEBUG = false;
+
private final Context mContext;
private final DvrDataManagerImpl mDataManager;
- private UpdateProgramTask mUpdateProgramTask;
+ private final ChannelDataManager mChannelDataManager;
private final Queue<Long> mProgramIdQueue = new LinkedList<>();
- private final ContentObserver mProgramsContentObserver = new ContentObserver(new Handler(
+ private QueryProgramTask mQueryProgramTask;
+ private final SeriesRecordingScheduler mSeriesRecordingScheduler;
+ private final ContentObserver mContentObserver = new ContentObserver(new Handler(
Looper.getMainLooper())) {
@SuppressLint("SwitchIntDef")
@Override
public void onChange(boolean selfChange, Uri uri) {
switch (TvProviderUriMatcher.match(uri)) {
case TvProviderUriMatcher.MATCH_PROGRAM:
+ if (DEBUG) Log.d(TAG, "onProgramsUpdated");
onProgramsUpdated();
break;
case TvProviderUriMatcher.MATCH_PROGRAM_ID:
- addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(
- ContentUris.parseId(uri)));
+ if (DEBUG) {
+ Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri));
+ }
+ onProgramUpdated(ContentUris.parseId(uri));
break;
}
}
};
+
+ private final ChannelDataManager.Listener mChannelDataManagerListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ start();
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ onChannelsUpdated();
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
+
private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() {
@Override
public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
for (ScheduledRecording schedule : schedules) {
addProgramIdToCheckIfNeeded(schedule);
}
+ startNextUpdateIfNeeded();
}
@Override
- public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { }
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ mProgramIdQueue.remove(schedule.getProgramId());
+ }
+ }
@Override
public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
for (ScheduledRecording schedule : schedules) {
mProgramIdQueue.remove(schedule.getProgramId());
+ addProgramIdToCheckIfNeeded(schedule);
}
+ startNextUpdateIfNeeded();
}
};
- public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager());
+ }
+
+ @VisibleForTesting
+ DvrDbSync(Context context, DvrDataManagerImpl dataManager,
+ ChannelDataManager channelDataManager) {
mContext = context;
mDataManager = dataManager;
+ mChannelDataManager = channelDataManager;
+ mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context);
}
/**
* Starts the DB sync.
*/
public void start() {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(mChannelDataManagerListener);
+ return;
+ }
mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
- mProgramsContentObserver);
+ mContentObserver);
mDataManager.addScheduledRecordingListener(mScheduleListener);
+ onChannelsUpdated();
onProgramsUpdated();
}
@@ -103,17 +162,51 @@ class DvrDbSync {
*/
public void stop() {
mProgramIdQueue.clear();
- if (mUpdateProgramTask != null) {
- mUpdateProgramTask.cancel(true);
+ if (mQueryProgramTask != null) {
+ mQueryProgramTask.cancel(true);
}
+ mChannelDataManager.removeListener(mChannelDataManagerListener);
mDataManager.removeScheduledRecordingListener(mScheduleListener);
- mContext.getContentResolver().unregisterContentObserver(mProgramsContentObserver);
+ mContext.getContentResolver().unregisterContentObserver(mContentObserver);
+ }
+
+ private void onChannelsUpdated() {
+ List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>();
+ for (SeriesRecording r : mDataManager.getSeriesRecordings()) {
+ if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
+ && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
+ seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r)
+ .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL)
+ .setState(SeriesRecording.STATE_SERIES_STOPPED).build());
+ }
+ }
+ if (!seriesRecordingsToUpdate.isEmpty()) {
+ mDataManager.updateSeriesRecording(
+ SeriesRecording.toArray(seriesRecordingsToUpdate));
+ }
+ List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
+ for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
+ if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
+ schedulesToRemove.add(r);
+ mProgramIdQueue.remove(r.getProgramId());
+ }
+ }
+ if (!schedulesToRemove.isEmpty()) {
+ mDataManager.removeScheduledRecording(
+ ScheduledRecording.toArray(schedulesToRemove));
+ }
}
private void onProgramsUpdated() {
- for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
+ for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) {
addProgramIdToCheckIfNeeded(schedule);
}
+ startNextUpdateIfNeeded();
+ }
+
+ private void onProgramUpdated(long programId) {
+ addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId));
+ startNextUpdateIfNeeded();
}
private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) {
@@ -125,34 +218,48 @@ class DvrDbSync {
&& !mProgramIdQueue.contains(programId)
&& (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
|| schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId);
mProgramIdQueue.offer(programId);
- startNextUpdateIfNeeded();
+ // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the
+ // schedule updates finish.
+ // Note that the SeriesRecordingScheduler should be paused even though the program to
+ // check is not episodic because it can be changed to the episodic program after the
+ // update, which affect the SeriesRecordingScheduler.
+ mSeriesRecordingScheduler.pauseUpdate();
}
}
private void startNextUpdateIfNeeded() {
- if (mProgramIdQueue.isEmpty()) {
+ if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) {
return;
}
- if (mUpdateProgramTask == null || mUpdateProgramTask.isCancelled()) {
- mUpdateProgramTask = new UpdateProgramTask(mProgramIdQueue.poll());
- mUpdateProgramTask.executeOnDbThread();
+ if (!mProgramIdQueue.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek());
+ mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll());
+ mQueryProgramTask.executeOnDbThread();
+ } else {
+ mSeriesRecordingScheduler.resumeUpdate();
}
}
@VisibleForTesting
void handleUpdateProgram(Program program, long programId) {
+ Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>();
ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId);
if (schedule != null
&& (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
|| schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
if (program == null) {
mDataManager.removeScheduledRecording(schedule);
+ if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
+ if (seriesRecording != null) {
+ seriesRecordingsToUpdate.add(seriesRecording);
+ }
+ }
} else {
long currentTimeMs = System.currentTimeMillis();
- // Change start time only when the recording start time has not passed.
- boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs
- && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule)
.setEndTimeMs(program.getEndTimeUtcMillis())
.setSeasonNumber(program.getSeasonNumber())
@@ -162,10 +269,51 @@ class DvrDbSync {
.setProgramLongDescription(program.getLongDescription())
.setProgramPosterArtUri(program.getPosterArtUri())
.setProgramThumbnailUri(program.getThumbnailUri());
+ boolean needUpdate = false;
+ // Check the series recording.
+ SeriesRecording seriesRecordingForOldSchedule =
+ mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
+ if (program.getSeriesId() != null) {
+ // New program belongs to a series.
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(program.getSeriesId());
+ if (seriesRecording == null) {
+ // The new program is episodic while the previous one isn't.
+ SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext)
+ .getDvrManager().addSeriesRecording(program,
+ Collections.singletonList(program),
+ SeriesRecording.STATE_SERIES_STOPPED);
+ builder.setSeriesRecordingId(newSeriesRecording.getId());
+ needUpdate = true;
+ } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
+ // The new program belongs to the other series.
+ builder.setSeriesRecordingId(seriesRecording.getId());
+ needUpdate = true;
+ seriesRecordingsToUpdate.add(seriesRecording);
+ if (seriesRecordingForOldSchedule != null) {
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ } else if (!Objects.equals(schedule.getSeasonNumber(),
+ program.getSeasonNumber())
+ || !Objects.equals(schedule.getEpisodeNumber(),
+ program.getEpisodeNumber())) {
+ // The episode number has been changed.
+ if (seriesRecordingForOldSchedule != null) {
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ }
+ } else if (seriesRecordingForOldSchedule != null) {
+ // Old program belongs to a series but the new one doesn't.
+ seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
+ }
+ // Change start time only when the recording start time has not passed.
+ boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs
+ && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
if (needToChangeStartTime) {
- mDataManager.updateScheduledRecording(
- builder.setStartTimeMs(program.getStartTimeUtcMillis()).build());
- } else if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
+ builder.setStartTimeMs(program.getStartTimeUtcMillis());
+ needUpdate = true;
+ }
+ if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
|| !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
|| !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
|| !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
@@ -179,27 +327,35 @@ class DvrDbSync {
program.getThumbnailUri())) {
mDataManager.updateScheduledRecording(builder.build());
}
+ if (!seriesRecordingsToUpdate.isEmpty()) {
+ // The series recordings will be updated after it's resumed.
+ mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate);
+ }
}
}
}
- private class UpdateProgramTask extends AsyncQueryProgramTask {
+ private class QueryProgramTask extends AsyncQueryProgramTask {
private final long mProgramId;
- public UpdateProgramTask(long programId) {
+ QueryProgramTask(long programId) {
super(mContext.getContentResolver(), programId);
mProgramId = programId;
}
@Override
protected void onCancelled(Program program) {
- mUpdateProgramTask = null;
+ if (mQueryProgramTask == this) {
+ mQueryProgramTask = null;
+ }
startNextUpdateIfNeeded();
}
@Override
protected void onPostExecute(Program program) {
- mUpdateProgramTask = null;
+ if (mQueryProgramTask == this) {
+ mQueryProgramTask = null;
+ }
handleUpdateProgram(program, mProgramId);
startNextUpdateIfNeeded();
}
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 48ca6eee..5fa6f90f 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -17,29 +17,36 @@
package com.android.tv.dvr;
import android.annotation.TargetApi;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
+import android.os.RemoteException;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import android.util.Range;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
-import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback;
+import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
+import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
+import com.android.tv.dvr.SeriesRecording.SeriesState;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Utils;
@@ -63,7 +70,6 @@ public class DvrManager {
private static final boolean DEBUG = false;
private final WritableDvrDataManager mDataManager;
- private final ChannelDataManager mChannelDataManager;
private final DvrScheduleManager mScheduleManager;
// @GuardedBy("mListener")
private final Map<Listener, Handler> mListener = new HashMap<>();
@@ -71,64 +77,124 @@ public class DvrManager {
public DvrManager(Context context) {
SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
+ mAppContext = context.getApplicationContext();
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
- mAppContext = context.getApplicationContext();
- mChannelDataManager = appSingletons.getChannelDataManager();
mScheduleManager = appSingletons.getDvrScheduleManager();
+ if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
+ createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms());
+ } else {
+ // No need to handle DVR schedule load finished because schedule manager is initialized
+ // after the all the schedules are loaded.
+ if (!mDataManager.isRecordedProgramLoadFinished()) {
+ mDataManager.addRecordedProgramLoadFinishedListener(
+ new OnRecordedProgramLoadFinishedListener() {
+ @Override
+ public void onRecordedProgramLoadFinished() {
+ mDataManager.removeRecordedProgramLoadFinishedListener(this);
+ if (mDataManager.isInitialized()
+ && mScheduleManager.isInitialized()) {
+ createSeriesRecordingsForRecordedProgramsIfNeeded(
+ mDataManager.getRecordedPrograms());
+ }
+ }
+ });
+ }
+ if (!mScheduleManager.isInitialized()) {
+ mScheduleManager.addOnInitializeListener(new OnInitializeListener() {
+ @Override
+ public void onInitialize() {
+ mScheduleManager.removeOnInitializeListener(this);
+ if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
+ createSeriesRecordingsForRecordedProgramsIfNeeded(
+ mDataManager.getRecordedPrograms());
+ }
+ }
+ });
+ }
+ }
+ mDataManager.addRecordedProgramListener(new RecordedProgramListener() {
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) {
+ return;
+ }
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ // Removing series recording is handled in the SeriesRecordingDetailsFragment.
+ }
+ });
+ }
+
+ private void createSeriesRecordingsForRecordedProgramsIfNeeded(
+ List<RecordedProgram> recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
+ }
+ }
+
+ private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) {
+ if (recordedProgram.getSeriesId() != null) {
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(recordedProgram.getSeriesId());
+ if (seriesRecording == null) {
+ addSeriesRecording(recordedProgram);
+ }
+ }
}
/**
* Schedules a recording for {@code program}.
*/
- public void addSchedule(Program program) {
+ public ScheduledRecording addSchedule(Program program) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- return;
- }
- TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program);
- if (input == null) {
- Log.e(TAG, "Can't find input for program: " + program);
- return;
+ return null;
}
- ScheduledRecording schedule;
SeriesRecording seriesRecording = getSeriesRecording(program);
- if (seriesRecording == null) {
- schedule = createScheduledRecordingBuilder(input.getId(), program)
- .setPriority(mScheduleManager.suggestNewPriority())
- .build();
- } else {
- schedule = createScheduledRecordingBuilder(input.getId(), program)
- .setPriority(seriesRecording.getPriority())
- .setSeriesRecordingId(seriesRecording.getId())
- .build();
- }
- mDataManager.addScheduledRecording(schedule);
+ return addSchedule(program, seriesRecording == null
+ ? mScheduleManager.suggestNewPriority()
+ : seriesRecording.getPriority());
}
/**
- * Schedules a recording for {@code program} instead of the list of recording that conflict.
- *
- * @param program the program to record
- * @param recordingsToOverride the possible empty list of recordings that will not be recorded
+ * Schedules a recording for {@code program} with the highest priority so that the schedule
+ * can be recorded.
*/
- public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) {
- Log.i(TAG, "Adding scheduled recording of " + program + " instead of " +
- recordingsToOverride);
+ public ScheduledRecording addScheduleWithHighestPriority(Program program) {
if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- return;
+ return null;
}
+ SeriesRecording seriesRecording = getSeriesRecording(program);
+ return addSchedule(program, seriesRecording == null
+ ? mScheduleManager.suggestNewPriority()
+ : mScheduleManager.suggestHighestPriority(seriesRecording.getInputId(),
+ new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()),
+ seriesRecording.getPriority()));
+ }
+
+ private ScheduledRecording addSchedule(Program program, long priority) {
TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program);
if (input == null) {
Log.e(TAG, "Can't find input for program: " + program);
- return;
+ return null;
}
- Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR);
- long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE
- : recordingsToOverride.get(0).getPriority() + 1;
- ScheduledRecording r = createScheduledRecordingBuilder(input.getId(), program)
+ ScheduledRecording schedule;
+ SeriesRecording seriesRecording = getSeriesRecording(program);
+ schedule = createScheduledRecordingBuilder(input.getId(), program)
.setPriority(priority)
+ .setSeriesRecordingId(seriesRecording == null ? SeriesRecording.ID_NOT_SET
+ : seriesRecording.getId())
.build();
- mDataManager.addScheduledRecording(r);
+ mDataManager.addScheduledRecording(schedule);
+ return schedule;
}
/**
@@ -148,6 +214,15 @@ public class DvrManager {
addScheduleInternal(input.getId(), channel.getId(), startTime, endTime);
}
+ /**
+ * Adds the schedule.
+ */
+ public void addSchedule(ScheduledRecording schedule) {
+ if (mDataManager.isDvrScheduleLoadFinished()) {
+ mDataManager.addScheduledRecording(schedule);
+ }
+ }
+
private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) {
mDataManager.addScheduledRecording(ScheduledRecording
.builder(inputId, channelId, startTime, endTime)
@@ -156,10 +231,10 @@ public class DvrManager {
}
/**
- * Adds a new series recording and schedules for the programs.
+ * Adds a new series recording and schedules for the programs with the initial state.
*/
public SeriesRecording addSeriesRecording(Program selectedProgram,
- List<Program> programsToSchedule) {
+ List<Program> programsToSchedule, @SeriesState int initialState) {
Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: "
+ programsToSchedule);
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
@@ -172,6 +247,7 @@ public class DvrManager {
}
SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram)
.setPriority(mScheduleManager.suggestNewSeriesPriority())
+ .setState(initialState)
.build();
mDataManager.addSeriesRecording(seriesRecording);
// The schedules for the recorded programs should be added not to create the schedule the
@@ -181,6 +257,18 @@ public class DvrManager {
return seriesRecording;
}
+ private void addSeriesRecording(RecordedProgram recordedProgram) {
+ SeriesRecording seriesRecording =
+ SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram)
+ .setPriority(mScheduleManager.suggestNewSeriesPriority())
+ .setState(SeriesRecording.STATE_SERIES_STOPPED)
+ .build();
+ mDataManager.addSeriesRecording(seriesRecording);
+ // The schedules for the recorded programs should be added not to create the schedule the
+ // duplicate episodes.
+ addRecordedProgramToSeriesRecording(seriesRecording);
+ }
+
private void addRecordedProgramToSeriesRecording(SeriesRecording series) {
List<ScheduledRecording> toAdd = new ArrayList<>();
for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) {
@@ -221,11 +309,8 @@ public class DvrManager {
mDataManager.getScheduledRecordingForProgramId(program.getId());
if (scheduleWithSameProgram != null) {
if (scheduleWithSameProgram.getState()
- == ScheduledRecording.STATE_RECORDING_NOT_STARTED
- || scheduleWithSameProgram.getState()
- == ScheduledRecording.STATE_RECORDING_CANCELED) {
+ == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram)
- .setPriority(series.getPriority())
.setSeriesRecordingId(series.getId())
.build();
if (!r.equals(scheduleWithSameProgram)) {
@@ -233,15 +318,10 @@ public class DvrManager {
}
}
} else {
- ScheduledRecording.Builder scheduledRecordingBuilder =
- createScheduledRecordingBuilder(input.getId(), program)
+ toAdd.add(createScheduledRecordingBuilder(input.getId(), program)
.setPriority(series.getPriority())
- .setSeriesRecordingId(series.getId());
- if (series.getState() == SeriesRecording.STATE_SERIES_CANCELED) {
- scheduledRecordingBuilder.setState(
- ScheduledRecording.STATE_RECORDING_CANCELED);
- }
- toAdd.add(scheduledRecordingBuilder.build());
+ .setSeriesRecordingId(series.getId())
+ .build());
}
}
if (!toAdd.isEmpty()) {
@@ -257,29 +337,33 @@ public class DvrManager {
*/
public void updateSeriesRecording(SeriesRecording series) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- // TODO: revise this method. b/30946239
- boolean isPreviousCanceled = false;
- long oldPriority = 0;
+ SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext);
+ scheduler.pauseUpdate();
SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
if (previousSeries != null) {
- isPreviousCanceled = previousSeries.getState()
- == SeriesRecording.STATE_SERIES_CANCELED;
- oldPriority = previousSeries.getPriority();
+ if (previousSeries.getChannelOption() != series.getChannelOption()
+ || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
+ && previousSeries.getChannelId() != series.getChannelId())) {
+ List<ScheduledRecording> schedules =
+ mDataManager.getScheduledRecordings(series.getId());
+ List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.isNotStarted()) {
+ schedulesToRemove.add(schedule);
+ }
+ }
+ mDataManager.removeScheduledRecording(true,
+ ScheduledRecording.toArray(schedulesToRemove));
+ }
}
mDataManager.updateSeriesRecording(series);
- if (!isPreviousCanceled && series.getState() == SeriesRecording.STATE_SERIES_CANCELED) {
- cancelScheduleToSeriesRecording(series);
- } else if (isPreviousCanceled
- && series.getState() == SeriesRecording.STATE_SERIES_NORMAL) {
- resumeScheduleToSeriesRecording(series);
- }
- if (oldPriority != series.getPriority()) {
+ if (previousSeries == null
+ || previousSeries.getPriority() != series.getPriority()) {
long priority = series.getPriority();
List<ScheduledRecording> schedulesToUpdate = new ArrayList<>();
for (ScheduledRecording schedule
: mDataManager.getScheduledRecordings(series.getId())) {
- if (schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
- && schedule.getStartTimeMs() > System.currentTimeMillis()) {
+ if (schedule.isNotStarted()) {
schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule)
.setPriority(priority).build());
}
@@ -289,59 +373,10 @@ public class DvrManager {
ScheduledRecording.toArray(schedulesToUpdate));
}
}
+ scheduler.resumeUpdate();
}
}
- private void cancelScheduleToSeriesRecording(SeriesRecording series) {
- List<ScheduledRecording> allRecordings = mDataManager.getAvailableScheduledRecordings();
- for (ScheduledRecording recording : allRecordings) {
- if (recording.getSeriesRecordingId() == series.getId()) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- stopRecording(recording);
- continue;
- }
- updateScheduledRecording(ScheduledRecording.buildFrom(recording).setState
- (ScheduledRecording.STATE_RECORDING_CANCELED).build());
- }
- }
- }
-
- private void resumeScheduleToSeriesRecording(SeriesRecording series) {
- List<ScheduledRecording> allRecording = mDataManager
- .getAvailableAndCanceledScheduledRecordings();
- for (ScheduledRecording recording : allRecording) {
- if (recording.getSeriesRecordingId() == series.getId()) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED &&
- recording.getEndTimeMs() > System.currentTimeMillis()) {
- updateScheduledRecording(ScheduledRecording.buildFrom(recording)
- .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED).build());
- }
- }
- }
- }
-
- /**
- * Queries the programs which belong to the same series as {@code seriesProgram}.
- * <p>
- * It's done in the background because it needs the DB access, and the callback will be called
- * when it finishes.
- */
- public void queryProgramsForSeries(Program seriesProgram, ProgramLoadCallback callback) {
- if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
- callback.onProgramLoadFinished(Collections.emptyList());
- return;
- }
- TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, seriesProgram);
- if (input == null) {
- Log.e(TAG, "Can't find input for program: " + seriesProgram);
- return;
- }
- SeriesRecordingScheduler.getInstance(mAppContext).queryPrograms(
- SeriesRecording.builder(input.getId(), seriesProgram)
- .setPriority(mScheduleManager.suggestNewPriority())
- .build(), callback);
- }
-
/**
* Removes the series recording and all the corresponding schedules which are not started yet.
*/
@@ -365,6 +400,33 @@ public class DvrManager {
}
/**
+ * Returns true, if the series recording can be removed. If a series recording is NORMAL state
+ * or has recordings or schedules, it cannot be removed.
+ */
+ public boolean canRemoveSeriesRecording(long seriesRecordingId) {
+ SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId);
+ if (seriesRecording == null) {
+ return false;
+ }
+ if (!seriesRecording.isStopped()) {
+ return false;
+ }
+ for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
+ if (r.getSeriesRecordingId() == seriesRecordingId) {
+ return false;
+ }
+ }
+ String seriesId = seriesRecording.getSeriesId();
+ SoftPreconditions.checkNotNull(seriesId);
+ for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
+ if (seriesId.equals(r.getSeriesId())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
* Stops the currently recorded program
*/
public void stopRecording(final ScheduledRecording recording) {
@@ -401,6 +463,23 @@ public class DvrManager {
}
/**
+ * Removes scheduled recordings without changing to the DELETED state.
+ */
+ public void forceRemoveScheduledRecording(ScheduledRecording... schedules) {
+ Log.i(TAG, "Force removing " + Arrays.asList(schedules));
+ if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return;
+ }
+ for (ScheduledRecording r : schedules) {
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ stopRecording(r);
+ } else {
+ mDataManager.removeScheduledRecording(true, r);
+ }
+ }
+ }
+
+ /**
* Removes the recorded program. It deletes the file if possible.
*/
public void removeRecordedProgram(Uri recordedProgramUri) {
@@ -434,51 +513,51 @@ public class DvrManager {
@Override
protected Void doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
- resolver.delete(recordedProgram.getUri(), null, null);
- try {
- Uri dataUri = recordedProgram.getDataUri();
- if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
- && dataUri.getPath() != null) {
- File recordedProgramPath = new File(dataUri.getPath());
- if (!recordedProgramPath.exists()) {
- if (DEBUG) Log.d(TAG, "File to delete not exist: "
- + recordedProgramPath);
- } else {
- Utils.deleteDirOrFile(recordedProgramPath);
- if (DEBUG) {
- Log.d(TAG, "Sucessfully deleted files of the recorded program: "
- + recordedProgram.getDataUri());
- }
+ int deletedCounts = resolver.delete(recordedProgram.getUri(), null, null);
+ if (deletedCounts > 0) {
+ // TODO: executeOnExecutor should be called on the main thread.
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ removeRecordedData(recordedProgram.getDataUri());
+ return null;
}
- }
- } catch (SecurityException e) {
- if (DEBUG) {
- Log.d(TAG, "To delete " + recordedProgram
- + "\nyou should manually delete video data at"
- + "\nadb shell rm -rf " + recordedProgram.getDataUri());
- }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return null;
}
}.executeOnDbThread();
}
- /**
- * Remove all recorded programs due to missing storage.
- *
- * @param inputId for the recorded programs to remove
- */
- public void removeRecordedProgramByMissingStorage(final String inputId) {
- if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
- return;
+ public void removeRecordedPrograms(List<Long> recordedProgramIds) {
+ final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
+ final List<Uri> dataUris = new ArrayList<>();
+ for (Long rId : recordedProgramIds) {
+ RecordedProgram r = mDataManager.getRecordedProgram(rId);
+ if (r != null) {
+ dataUris.add(r.getDataUri());
+ dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build());
+ }
}
new AsyncDbTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
- String args[] = { inputId };
- resolver.delete(TvContract.RecordedPrograms.CONTENT_URI,
- TvContract.RecordedPrograms.COLUMN_INPUT_ID + " = ?", args);
+ try {
+ resolver.applyBatch(TvContract.AUTHORITY, dbOperations);
+ // TODO: executeOnExecutor should be called on the main thread.
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ for (Uri dataUri : dataUris) {
+ removeRecordedData(dataUri);
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.w(TAG, "Remove reocrded programs from DB failed.", e);
+ }
return null;
}
}.executeOnDbThread();
@@ -527,10 +606,9 @@ public class DvrManager {
* {@code false}.
*/
public boolean isConflicting(ScheduledRecording schedule) {
- if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- return false;
- }
- return mScheduleManager.isConflicting(schedule);
+ return schedule != null
+ && SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())
+ && mScheduleManager.isConflicting(schedule);
}
/**
@@ -547,20 +625,26 @@ public class DvrManager {
}
/**
- * Returns the earliest end time of the current recording for the TV input. If there are no
- * recordings, Long.MAX_VALUE is returned.
+ * Sets the highest priority to the schedule.
*/
- public long getEarliestRecordingEndTime(String inputId) {
- long result = Long.MAX_VALUE;
- for (ScheduledRecording schedule : mDataManager.getStartedRecordings()) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext,
- schedule.getChannelId());
- if (input != null && input.getId().equals(inputId)
- && schedule.getEndTimeMs() < result) {
- result = schedule.getEndTimeMs();
+ public void setHighestPriority(ScheduledRecording schedule) {
+ if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ long newPriority = mScheduleManager.suggestHighestPriority(schedule);
+ if (newPriority != schedule.getPriority()) {
+ mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule)
+ .setPriority(newPriority).build());
}
}
- return result;
+ }
+
+ /**
+ * Suggests the higher priority than the schedules which overlap with {@code schedule}.
+ */
+ public long suggestHighestPriority(ScheduledRecording schedule) {
+ if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
+ return mScheduleManager.suggestHighestPriority(schedule);
+ }
+ return DvrScheduleManager.DEFAULT_PRIORITY;
}
/**
@@ -622,6 +706,23 @@ public class DvrManager {
}
/**
+ * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to
+ * the series recording {@code seriesRecordingId}.
+ */
+ public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) {
+ if (!mDataManager.isDvrScheduleLoadFinished()) {
+ return Collections.emptyList();
+ }
+ List<ScheduledRecording> schedules = new ArrayList<>();
+ for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) {
+ if (schedule.isInProgress() || schedule.isNotStarted()) {
+ schedules.add(schedule);
+ }
+ }
+ return schedules;
+ }
+
+ /**
* Returns the series recording related to the program.
*/
@Nullable
@@ -704,6 +805,40 @@ public class DvrManager {
return null;
}
+ @WorkerThread
+ private void removeRecordedData(Uri dataUri) {
+ try {
+ if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
+ && dataUri.getPath() != null) {
+ File recordedProgramPath = new File(dataUri.getPath());
+ if (!recordedProgramPath.exists()) {
+ if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
+ } else {
+ Utils.deleteDirOrFile(recordedProgramPath);
+ if (DEBUG) {
+ Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
+ }
+ }
+ }
+ } catch (SecurityException e) {
+ if (DEBUG) {
+ Log.d(TAG, "To delete this recorded program, please manually delete video data at"
+ + "\nadb shell rm -rf " + dataUri);
+ }
+ }
+ }
+
+ /**
+ * Remove all the records related to the input.
+ * <p>
+ * Note that this should be called after the input was removed.
+ */
+ public void forgetStorage(String inputId) {
+ if (mDataManager.isInitialized()) {
+ mDataManager.forgetStorage(inputId);
+ }
+ }
+
/**
* Listener internally used inside dvr package.
*/
diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java
index 3320e0fd..5deda44a 100644
--- a/src/com/android/tv/dvr/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/DvrPlaybackActivity.java
@@ -23,6 +23,7 @@ import android.os.Bundle;
import android.util.Log;
import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
/**
@@ -36,6 +37,7 @@ public class DvrPlaybackActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_playback);
diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
index da815712..9759a856 100644
--- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
@@ -17,6 +17,7 @@
package com.android.tv.dvr;
import android.app.Activity;
+import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadata;
@@ -32,8 +33,10 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TimeShiftUtils;
+import com.android.tv.util.Utils;
public class DvrPlaybackMediaSessionHelper {
private static final String TAG = "DvrPlaybackMediaSessionHelper";
@@ -50,8 +53,8 @@ public class DvrPlaybackMediaSessionHelper {
private final DvrWatchedPositionManager mDvrWatchedPositionManager;
private final ChannelDataManager mChannelDataManager;
- public DvrPlaybackMediaSessionHelper(Activity activity,
- String mediaSessionTag, DvrPlayer dvrPlayer) {
+ public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag,
+ DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) {
mActivity = activity;
mDvrPlayer = dvrPlayer;
mDvrWatchedPositionManager =
@@ -71,6 +74,21 @@ public class DvrPlaybackMediaSessionHelper {
.setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs);
}
}
+
+ @Override
+ public void onPlaybackEnded() {
+ // TODO: Deal with watched over recordings in DVR library
+ RecordedProgram nextEpisode =
+ overlayFragment.getNextEpisode(mDvrPlayer.getProgram());
+ if (nextEpisode == null) {
+ mDvrPlayer.reset();
+ mActivity.finish();
+ } else {
+ Intent intent = new Intent(activity, DvrPlaybackActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId());
+ mActivity.startActivity(intent);
+ }
+ }
});
initializeMediaSession(mediaSessionTag);
}
@@ -122,7 +140,7 @@ public class DvrPlaybackMediaSessionHelper {
* Checks if the recorded program is the same as now playing one.
*/
public boolean isCurrentProgram(RecordedProgram program) {
- return program == null ? false : program.equals(getProgram());
+ return program != null && program.equals(getProgram());
}
/**
@@ -216,7 +234,7 @@ public class DvrPlaybackMediaSessionHelper {
private void updateMediaMetadata(final long programId, final String title,
final String subtitle, final long duration,
final Bitmap posterArt, final int imageResId) {
- new AsyncTask<Void, Void, Void> () {
+ new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg0) {
MediaMetadata.Builder builder = new MediaMetadata.Builder();
@@ -252,16 +270,23 @@ public class DvrPlaybackMediaSessionHelper {
@Override
public void onPlay() {
- mDvrPlayer.play();
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.play();
+ }
}
@Override
public void onPause() {
- mDvrPlayer.pause();
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.pause();
+ }
}
@Override
public void onFastForward() {
+ if (!mDvrPlayer.isPlaybackPrepared()) {
+ return;
+ }
if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) {
if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
mSpeedLevel++;
@@ -277,6 +302,9 @@ public class DvrPlaybackMediaSessionHelper {
@Override
public void onRewind() {
+ if (!mDvrPlayer.isPlaybackPrepared()) {
+ return;
+ }
if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) {
if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) {
mSpeedLevel++;
@@ -291,7 +319,9 @@ public class DvrPlaybackMediaSessionHelper {
@Override
public void onSeekTo(long positionMs) {
- mDvrPlayer.seekTo(positionMs);
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.seekTo(positionMs);
+ }
}
}
}
diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java
index 027d99f4..5656655c 100644
--- a/src/com/android/tv/dvr/DvrPlayer.java
+++ b/src/com/android/tv/dvr/DvrPlayer.java
@@ -55,6 +55,8 @@ public class DvrPlayer {
private boolean mPauseOnPrepared;
private final PlaybackParams mPlaybackParams = new PlaybackParams();
private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback();
+ private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
+ private boolean mTimeShiftPlayAvailable;
public static class DvrPlayerCallback {
/**
@@ -67,6 +69,10 @@ public class DvrPlayer {
* Called when the playback state or the playback speed is changed.
*/
public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { }
+ /**
+ * Called when the playback toward the end.
+ */
+ public void onPlaybackEnded() { }
}
public interface AspectRatioChangedListener {
@@ -208,7 +214,7 @@ public class DvrPlayer {
}
positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS);
if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs);
- mTvView.timeShiftSeekTo(positionMs);
+ mTvView.timeShiftSeekTo(positionMs + mStartPositionMs);
if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING ||
mPlaybackState == PlaybackState.STATE_REWINDING) {
mPlaybackState = PlaybackState.STATE_PLAYING;
@@ -222,12 +228,14 @@ public class DvrPlayer {
*/
public void reset() {
if (DEBUG) Log.d(TAG, "reset()");
- mTvView.reset();
+ mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1);
mPlaybackState = PlaybackState.STATE_NONE;
+ mTvView.reset();
+ mTimeShiftPlayAvailable = false;
+ mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
mTimeShiftCurrentPositionMs = 0;
mPlaybackParams.setSpeed(1.0f);
mProgram = null;
- mCallback.onPlaybackStateChanged(mPlaybackState, 1);
}
/**
@@ -317,43 +325,50 @@ public class DvrPlayer {
private void setTvViewCallbacks() {
mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() {
@Override
+ public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
+ if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs);
+ mStartPositionMs = timeMs;
+ if (mTimeShiftPlayAvailable) {
+ resumeToWatchedPositionIfNeeded();
+ }
+ }
+
+ @Override
public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
- // Workaround solution for b/29994826:
- // prevents rewinding and fast-forwarding over the ends.
+ if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs);
+ if (!mTimeShiftPlayAvailable) {
+ // Workaround of b/31436263
+ return;
+ }
+ // Workaround of b/32211561, TIF won't report start position when TIS report
+ // its start position as 0. In that case, we have to do the prework of playback
+ // on the first time we get current position, and the start position should be 0
+ // at that time.
+ if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mStartPositionMs = 0;
+ resumeToWatchedPositionIfNeeded();
+ }
+ timeMs -= mStartPositionMs;
if (mPlaybackState == PlaybackState.STATE_REWINDING
&& timeMs <= REWIND_POSITION_MARGIN_MS) {
play();
- } else if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
- && timeMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) {
- mTvView.timeShiftSeekTo(mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS);
- pause();
- }
- else {
+ } else {
mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
+ if (timeMs >= mProgram.getDurationMillis()) {
+ pause();
+ mCallback.onPlaybackEnded();
+ }
}
}
});
mTvView.setCallback(new TvView.TvInputCallback() {
@Override
public void onTimeShiftStatusChanged(String inputId, int status) {
- if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged");
+ if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status);
if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
&& mPlaybackState == PlaybackState.STATE_CONNECTING) {
- if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
- mTvView.timeShiftSeekTo(getRealSeekPosition(
- mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS));
- mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
- }
- if (mPauseOnPrepared) {
- mTvView.timeShiftPause();
- mPlaybackState = PlaybackState.STATE_PAUSED;
- mPauseOnPrepared = false;
- } else {
- mTvView.timeShiftResume();
- mPlaybackState = PlaybackState.STATE_PLAYING;
- }
- mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ mTimeShiftPlayAvailable = true;
}
}
@@ -390,4 +405,21 @@ public class DvrPlayer {
}
});
}
+
+ private void resumeToWatchedPositionIfNeeded() {
+ if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs,
+ SEEK_POSITION_MARGIN_MS) + mStartPositionMs);
+ mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
+ }
+ if (mPauseOnPrepared) {
+ mTvView.timeShiftPause();
+ mPlaybackState = PlaybackState.STATE_PAUSED;
+ mPauseOnPrepared = false;
+ } else {
+ mTvView.timeShiftResume();
+ mPlaybackState = PlaybackState.STATE_PLAYING;
+ }
+ mCallback.onPlaybackStateChanged(mPlaybackState, 1);
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java
index 39be7961..8c40aaa8 100644
--- a/src/com/android/tv/dvr/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/DvrRecordingService.java
@@ -70,7 +70,6 @@ public class DvrRecordingService extends Service {
super.onCreate();
SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
ApplicationSingletons singletons = TvApplication.getSingletons(this);
- DvrManager dvrManager = singletons.getDvrManager();
WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager();
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
@@ -103,7 +102,7 @@ public class DvrRecordingService extends Service {
mScheduler.stop();
mScheduler = null;
if (mHandlerThread != null) {
- mHandlerThread.quit();
+ mHandlerThread.quitSafely();
mHandlerThread = null;
}
super.onDestroy();
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index aa77c400..a5851a75 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -24,6 +24,7 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
+import android.util.LongSparseArray;
import android.util.Range;
import com.android.tv.ApplicationSingletons;
@@ -34,16 +35,18 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.util.CompositeComparator;
import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* A class to manage the schedules.
@@ -64,15 +67,34 @@ public class DvrScheduleManager {
// The new priority will have the offset from the existing one.
private static final long PRIORITY_OFFSET = 1024;
+ private static final Comparator<ScheduledRecording> RESULT_COMPARATOR =
+ new CompositeComparator<>(
+ ScheduledRecording.PRIORITY_COMPARATOR.reversed(),
+ ScheduledRecording.START_TIME_COMPARATOR,
+ ScheduledRecording.ID_COMPARATOR.reversed());
+
+ // The candidate comparator should be the consistent with
+ // InputTaskScheduler#CANDIDATE_COMPARATOR.
+ private static final Comparator<ScheduledRecording> CANDIDATE_COMPARATOR =
+ new CompositeComparator<>(
+ ScheduledRecording.PRIORITY_COMPARATOR,
+ ScheduledRecording.END_TIME_COMPARATOR,
+ ScheduledRecording.ID_COMPARATOR);
+
private final Context mContext;
private final DvrDataManagerImpl mDataManager;
private final ChannelDataManager mChannelDataManager;
private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>();
- private final Map<String, List<ScheduledRecording>> mInputConflictMap = new HashMap<>();
+ // The inner map is a hash map from scheduled recording to its conflicting status, i.e.,
+ // the boolean value true denotes the schedule is just partially conflicting, which means
+ // although there's conflictit, it might still be recorded partially.
+ private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap =
+ new HashMap<>();
private boolean mInitialized;
+ private final Set<OnInitializeListener> mOnInitializeListeners = new CopyOnWriteArraySet<>();
private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>();
private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners =
new ArraySet<>();
@@ -106,10 +128,13 @@ public class DvrScheduleManager {
if (!schedule.isNotStarted() && !schedule.isInProgress()) {
continue;
}
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext,
- schedule.getChannelId());
- if (input == null) {
+ TvInputInfo input = Utils
+ .getTvInputInfoForInputId(mContext, schedule.getInputId());
+ if (!SoftPreconditions.checkArgument(input != null, TAG,
+ "Input was removed for : " + schedule)) {
// Input removed.
+ mInputScheduleMap.remove(schedule.getInputId());
+ mInputConflictInfoMap.remove(schedule.getInputId());
continue;
}
String inputId = input.getId();
@@ -131,9 +156,11 @@ public class DvrScheduleManager {
}
for (ScheduledRecording schedule : scheduledRecordings) {
TvInputInfo input = Utils
- .getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ .getTvInputInfoForInputId(mContext, schedule.getInputId());
if (input == null) {
// Input removed.
+ mInputScheduleMap.remove(schedule.getInputId());
+ mInputConflictInfoMap.remove(schedule.getInputId());
continue;
}
String inputId = input.getId();
@@ -144,6 +171,14 @@ public class DvrScheduleManager {
mInputScheduleMap.remove(inputId);
}
}
+ Map<ScheduledRecording, Boolean> conflictInfo =
+ mInputConflictInfoMap.get(inputId);
+ if (conflictInfo != null) {
+ conflictInfo.remove(schedule);
+ if (conflictInfo.isEmpty()) {
+ mInputConflictInfoMap.remove(inputId);
+ }
+ }
}
onSchedulesChanged();
notifyScheduledRecordingRemoved(scheduledRecordings);
@@ -157,9 +192,12 @@ public class DvrScheduleManager {
}
for (ScheduledRecording schedule : scheduledRecordings) {
TvInputInfo input = Utils
- .getTvInputInfoForChannelId(mContext, schedule.getChannelId());
- if (input == null) {
+ .getTvInputInfoForInputId(mContext, schedule.getInputId());
+ if (!SoftPreconditions.checkArgument(input != null, TAG,
+ "Input was removed for : " + schedule)) {
// Input removed.
+ mInputScheduleMap.remove(schedule.getInputId());
+ mInputConflictInfoMap.remove(schedule.getInputId());
continue;
}
String inputId = input.getId();
@@ -170,8 +208,7 @@ public class DvrScheduleManager {
}
// Compare ID because ScheduledRecording.equals() doesn't work if the state
// is changed.
- Iterator<ScheduledRecording> i = schedules.iterator();
- while (i.hasNext()) {
+ for (Iterator<ScheduledRecording> i = schedules.iterator(); i.hasNext(); ) {
if (i.next().getId() == schedule.getId()) {
i.remove();
break;
@@ -183,6 +220,24 @@ public class DvrScheduleManager {
if (schedules.isEmpty()) {
mInputScheduleMap.remove(inputId);
}
+ // Update conflict list as well
+ Map<ScheduledRecording, Boolean> conflictInfo =
+ mInputConflictInfoMap.get(inputId);
+ if (conflictInfo != null) {
+ // Compare ID because ScheduledRecording.equals() doesn't work if the state
+ // is changed.
+ ScheduledRecording oldSchedule = null;
+ for (ScheduledRecording s : conflictInfo.keySet()) {
+ if (s.getId() == schedule.getId()) {
+ oldSchedule = s;
+ break;
+ }
+ }
+ if (oldSchedule != null) {
+ conflictInfo.put(schedule, conflictInfo.get(oldSchedule));
+ conflictInfo.remove(oldSchedule);
+ }
+ }
}
onSchedulesChanged();
notifyScheduledRecordingStatusChanged(scheduledRecordings);
@@ -249,33 +304,39 @@ public class DvrScheduleManager {
schedules.add(schedule);
}
}
- mInitialized = true;
+ if (!mInitialized) {
+ mInitialized = true;
+ notifyInitialize();
+ }
onSchedulesChanged();
}
private void onSchedulesChanged() {
+ // TODO: notify conflict state change when some conflicting recording becomes partially
+ // conflicting, vice versa.
List<ScheduledRecording> addedConflicts = new ArrayList<>();
List<ScheduledRecording> removedConflicts = new ArrayList<>();
for (String inputId : mInputScheduleMap.keySet()) {
- List<ScheduledRecording> oldConflicts = mInputConflictMap.get(inputId);
+ Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId);
Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>();
- if (oldConflicts != null) {
- for (ScheduledRecording r : oldConflicts) {
+ if (oldConflictsInfo != null) {
+ for (ScheduledRecording r : oldConflictsInfo.keySet()) {
oldConflictMap.put(r.getId(), r);
}
}
- List<ScheduledRecording> conflicts = getConflictingSchedules(inputId);
- for (ScheduledRecording r : conflicts) {
- if (oldConflictMap.remove(r.getId()) == null) {
- addedConflicts.add(r);
+ Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId);
+ if (conflictInfo.isEmpty()) {
+ mInputConflictInfoMap.remove(inputId);
+ } else {
+ mInputConflictInfoMap.put(inputId, conflictInfo);
+ List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet());
+ for (ScheduledRecording r : conflicts) {
+ if (oldConflictMap.remove(r.getId()) == null) {
+ addedConflicts.add(r);
+ }
}
}
removedConflicts.addAll(oldConflictMap.values());
- if (conflicts.isEmpty()) {
- mInputConflictMap.remove(inputId);
- } else {
- mInputConflictMap.put(inputId, conflicts);
- }
}
if (!removedConflicts.isEmpty()) {
notifyConflictStateChange(false, ScheduledRecording.toArray(removedConflicts));
@@ -334,6 +395,29 @@ public class DvrScheduleManager {
}
/**
+ * Adds a {@link OnInitializeListener}.
+ */
+ public final void addOnInitializeListener(OnInitializeListener listener) {
+ mOnInitializeListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link OnInitializeListener}.
+ */
+ public final void removeOnInitializeListener(OnInitializeListener listener) {
+ mOnInitializeListeners.remove(listener);
+ }
+
+ /**
+ * Calls {@link OnInitializeListener#onInitialize} for each listener.
+ */
+ private void notifyInitialize() {
+ for (OnInitializeListener l : mOnInitializeListeners) {
+ l.onInitialize();
+ }
+ }
+
+ /**
* Adds a {@link OnConflictStateChangeListener}.
*/
public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) {
@@ -380,6 +464,47 @@ public class DvrScheduleManager {
}
/**
+ * Suggests the higher priority than the schedules which overlap with {@code schedule}.
+ */
+ public long suggestHighestPriority(ScheduledRecording schedule) {
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId());
+ if (schedules == null) {
+ return DEFAULT_PRIORITY;
+ }
+ long highestPriority = Long.MIN_VALUE;
+ for (ScheduledRecording r : schedules) {
+ if (!r.equals(schedule) && r.isOverLapping(schedule)
+ && r.getPriority() > highestPriority) {
+ highestPriority = r.getPriority();
+ }
+ }
+ if (highestPriority == Long.MIN_VALUE || highestPriority < schedule.getPriority()) {
+ return schedule.getPriority();
+ }
+ return highestPriority + PRIORITY_OFFSET;
+ }
+
+ /**
+ * Suggests the higher priority than the schedules which overlap with {@code schedule}.
+ */
+ public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) {
+ List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId);
+ if (schedules == null) {
+ return DEFAULT_PRIORITY;
+ }
+ long highestPriority = Long.MIN_VALUE;
+ for (ScheduledRecording r : schedules) {
+ if (r.isOverLapping(peroid) && r.getPriority() > highestPriority) {
+ highestPriority = r.getPriority();
+ }
+ }
+ if (highestPriority == Long.MIN_VALUE || highestPriority < basePriority) {
+ return basePriority;
+ }
+ return highestPriority + PRIORITY_OFFSET;
+ }
+
+ /**
* Returns the priority for a series recording.
* <p>
* The recording will have the higher priority than the existing series.
@@ -411,11 +536,12 @@ public class DvrScheduleManager {
}
/**
- * Returns priority ordered list of all scheduled recordings that will not be recorded if
- * this program is.
+ * Returns a sorted list of all scheduled recordings that will not be recorded if
+ * this program is going to be recorded, with their priorities in decending order.
* <p>
- * Any empty list means there is no conflicts. If there is conflict the program must be
- * scheduled to record with a priority higher than the first recording in the list returned.
+ * An empty list means there is no conflicts. If there is conflict, a priority higher than
+ * the first recording in the returned list should be assigned to the new schedule of this
+ * program to guarantee the program would be completely recorded.
*/
public List<ScheduledRecording> getConflictingSchedules(Program program) {
SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
@@ -439,11 +565,34 @@ public class DvrScheduleManager {
}
/**
- * Returns priority ordered list of all scheduled recordings that will not be recorded if
- * this channel is.
+ * Returns list of all conflicting scheduled recordings with schedules belonging to {@code
+ * seriesRecording}
+ * recording.
+ * <p>
+ * Any empty list means there is no conflicts.
+ */
+ public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ SoftPreconditions.checkState(seriesRecording != null, TAG, "series recording is null");
+ if (!mInitialized || seriesRecording == null) {
+ return Collections.emptyList();
+ }
+ TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, seriesRecording.getInputId());
+ if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
+ return Collections.emptyList();
+ }
+ List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings(
+ seriesRecording.getId());
+ return getConflictingSchedules(input, schedulesForSeries);
+ }
+
+ /**
+ * Returns a sorted list of all scheduled recordings that will not be recorded if
+ * this channel is going to be recorded, with their priority in decending order.
* <p>
- * Any empty list means there is no conflicts. If there is conflict the channel must be
- * scheduled to record with a priority higher than the first recording in the list returned.
+ * An empty list means there is no conflicts. If there is conflict, a priority higher than
+ * the first recording in the returned list should be assigned to the new schedule of this
+ * channel to guarantee the channel would be completely recorded in the designated time range.
*/
public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs,
long endTimeMs) {
@@ -468,18 +617,18 @@ public class DvrScheduleManager {
* the given input.
*/
@NonNull
- private List<ScheduledRecording> getConflictingSchedules(String inputId) {
+ private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) {
SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId);
SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId);
if (!mInitialized || input == null) {
- return Collections.emptyList();
+ return Collections.emptyMap();
}
List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
if (schedules == null || schedules.isEmpty()) {
- return Collections.emptyList();
+ return Collections.emptyMap();
}
- return getConflictingSchedules(schedules, input.getTunerCount());
+ return getConflictingSchedulesInfo(schedules, input.getTunerCount());
}
/**
@@ -490,14 +639,33 @@ public class DvrScheduleManager {
*/
public boolean isConflicting(ScheduledRecording schedule) {
SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
+ SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : "
+ + schedule.getChannelId());
+ if (!mInitialized || input == null) {
+ return false;
+ }
+ Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
+ return conflicts != null && conflicts.containsKey(schedule);
+ }
+
+ /**
+ * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be
+ * recorded even if the priority of the schedule is not raised.
+ * <p>
+ * If the given schedule is not conflicting or is totally conflicting, i.e., cannot be recorded
+ * at all, this method returns {@code false} in both cases.
+ */
+ public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) {
+ SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
+ TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : "
+ schedule.getChannelId());
if (!mInitialized || input == null) {
return false;
}
- List<ScheduledRecording> conflicts = mInputConflictMap.get(input.getId());
- return conflicts != null && conflicts.contains(schedule);
+ Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
+ return conflicts != null && conflicts.getOrDefault(schedule, false);
}
/**
@@ -599,7 +767,7 @@ public class DvrScheduleManager {
List<ScheduledRecording> result = new ArrayList<>();
result.addAll(getConflictingSchedules(schedulesSameChannel, 1));
result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount));
- Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR);
+ Collections.sort(result, RESULT_COMPARATOR);
return result;
}
@@ -639,66 +807,161 @@ public class DvrScheduleManager {
*/
public static List<ScheduledRecording> getConflictingSchedules(
List<ScheduledRecording> schedules, int tunerCount) {
- return getConflictingSchedules(schedules, tunerCount,
- Collections.singletonList(new Range<>(Long.MIN_VALUE, Long.MAX_VALUE)));
+ return getConflictingSchedules(schedules, tunerCount, null);
}
@VisibleForTesting
- static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedules,
- int tunerCount, List<Range<Long>> periods) {
- List<ScheduledRecording> schedulesToCheck = new ArrayList<>();
- // Filter out non-overlapping or empty duration of schedules.
- for (ScheduledRecording schedule : schedules) {
- for (Range<Long> period : periods) {
- if (schedule.isOverLapping(period)
- && schedule.getStartTimeMs() < schedule.getEndTimeMs()) {
- schedulesToCheck.add(schedule);
- break;
+ static List<ScheduledRecording> getConflictingSchedules(
+ List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
+ List<ScheduledRecording> result = new ArrayList<>(
+ getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet());
+ Collections.sort(result, RESULT_COMPARATOR);
+ return result;
+ }
+
+ @VisibleForTesting
+ static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
+ List<ScheduledRecording> schedules, int tunerCount) {
+ return getConflictingSchedulesInfo(schedules, tunerCount, null);
+ }
+
+ /**
+ * This is the core method to calculate all the conflicting schedules (in given periods).
+ * <p>
+ * Note that this method will ignore duplicated schedules with a same hash code. (Please refer
+ * to {@link ScheduledRecording#hashCode}.)
+ *
+ * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean
+ * value denotes if the scheduled recording is partially conflicting, i.e., is possible
+ * to be partially recorded under the given schedules and tuner count {@code true},
+ * or not {@code false}.
+ */
+ private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
+ List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
+ List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
+ // Sort by the same order as that in InputTaskScheduler.
+ Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator());
+ List<ScheduledRecording> recordings = new ArrayList<>();
+ Map<ScheduledRecording, Boolean> conflicts = new HashMap<>();
+ Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>();
+ // Simulate InputTaskScheduler.
+ while (!schedulesToCheck.isEmpty()) {
+ ScheduledRecording schedule = schedulesToCheck.remove(0);
+ removeFinishedRecordings(recordings, schedule.getStartTimeMs());
+ if (recordings.size() < tunerCount) {
+ recordings.add(schedule);
+ if (modified2OriginalSchedules.containsKey(schedule)) {
+ // Schedule has been modified, which means it's already conflicted.
+ // Modify its state to partially conflicted.
+ conflicts.put(modified2OriginalSchedules.get(schedule), true);
}
- }
- }
- // Sort by the end time.
- // If a.end <= b.end <= c.end and a overlaps with b and c, then b overlaps with c.
- // Likewise, if a1.end <= a2.end <= ... , all the schedules which overlap with a1 overlap
- // with each other.
- Collections.sort(schedulesToCheck, ScheduledRecording.END_TIME_COMPARATOR);
- Set<ScheduledRecording> conflicts = new ArraySet<>();
- List<ScheduledRecording> overlaps = new ArrayList<>();
- for (int i = 0; i < schedulesToCheck.size(); ++i) {
- ScheduledRecording r1 = schedulesToCheck.get(i);
- if (conflicts.contains(r1)) {
- // No need to check r1 because it's a conflicting schedule already.
- continue;
- }
- overlaps.clear();
- overlaps.add(r1);
- // Find schedules which overlap with r1.
- for (int j = i + 1; j < schedulesToCheck.size(); ++j) {
- ScheduledRecording r2 = schedulesToCheck.get(j);
- if (!conflicts.contains(r2) && r1.getEndTimeMs() > r2.getStartTimeMs()) {
- overlaps.add(r2);
+ } else {
+ ScheduledRecording candidate = findReplaceableRecording(recordings, schedule);
+ if (candidate != null) {
+ if (!modified2OriginalSchedules.containsKey(candidate)) {
+ conflicts.put(candidate, true);
+ }
+ recordings.remove(candidate);
+ recordings.add(schedule);
+ if (modified2OriginalSchedules.containsKey(schedule)) {
+ // Schedule has been modified, which means it's already conflicted.
+ // Modify its state to partially conflicted.
+ conflicts.put(modified2OriginalSchedules.get(schedule), true);
+ }
+ } else {
+ if (!modified2OriginalSchedules.containsKey(schedule)) {
+ // if schedule has been modified, it's already conflicted.
+ // No need to add it again.
+ conflicts.put(schedule, false);
+ }
+ long earliestEndTime = getEarliestEndTime(recordings);
+ if (earliestEndTime < schedule.getEndTimeMs()) {
+ // The schedule can starts when other recording ends even though it's
+ // clipped.
+ ScheduledRecording modifiedSchedule = ScheduledRecording.buildFrom(schedule)
+ .setStartTimeMs(earliestEndTime).build();
+ ScheduledRecording originalSchedule =
+ modified2OriginalSchedules.getOrDefault(schedule, schedule);
+ modified2OriginalSchedules.put(modifiedSchedule, originalSchedule);
+ int insertPosition = Collections.binarySearch(schedulesToCheck,
+ modifiedSchedule,
+ ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
+ if (insertPosition >= 0) {
+ schedulesToCheck.add(insertPosition, modifiedSchedule);
+ } else {
+ schedulesToCheck.add(-insertPosition - 1, modifiedSchedule);
+ }
+ }
}
}
- Collections.sort(overlaps, ScheduledRecording.PRIORITY_COMPARATOR);
- // If there are more than one overlapping schedules for the same channel, only one
- // schedule will be recorded.
- HashSet<Long> channelIds = new HashSet<>();
- for (Iterator<ScheduledRecording> iter = overlaps.iterator(); iter.hasNext(); ) {
+ }
+ // Returns only the schedules with the given range.
+ if (periods != null && !periods.isEmpty()) {
+ for (Iterator<ScheduledRecording> iter = conflicts.keySet().iterator();
+ iter.hasNext(); ) {
+ boolean overlapping = false;
ScheduledRecording schedule = iter.next();
- if (channelIds.contains(schedule.getChannelId())) {
- conflicts.add(schedule);
+ for (Range<Long> period : periods) {
+ if (schedule.isOverLapping(period)) {
+ overlapping = true;
+ break;
+ }
+ }
+ if (!overlapping) {
iter.remove();
- } else {
- channelIds.add(schedule.getChannelId());
}
}
- if (overlaps.size() > tunerCount) {
- conflicts.addAll(overlaps.subList(tunerCount, overlaps.size()));
+ }
+ return conflicts;
+ }
+
+ private static void removeFinishedRecordings(List<ScheduledRecording> recordings,
+ long currentTimeMs) {
+ for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) {
+ if (iter.next().getEndTimeMs() <= currentTimeMs) {
+ iter.remove();
}
}
- List<ScheduledRecording> result = new ArrayList<>(conflicts);
- Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR);
- return result;
+ }
+
+ /**
+ * @see InputTaskScheduler#getReplacableTask
+ */
+ private static ScheduledRecording findReplaceableRecording(List<ScheduledRecording> recordings,
+ ScheduledRecording schedule) {
+ // Returns the recording with the following priority.
+ // 1. The recording with the lowest priority is returned.
+ // 2. If the priorities are the same, the recording which finishes early is returned.
+ // 3. If 1) and 2) are the same, the early created schedule is returned.
+ ScheduledRecording candidate = null;
+ for (ScheduledRecording recording : recordings) {
+ if (schedule.getPriority() > recording.getPriority()) {
+ if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) {
+ candidate = recording;
+ }
+ }
+ }
+ return candidate;
+ }
+
+ private static long getEarliestEndTime(List<ScheduledRecording> recordings) {
+ long earliest = Long.MAX_VALUE;
+ for (ScheduledRecording recording : recordings) {
+ if (earliest > recording.getEndTimeMs()) {
+ earliest = recording.getEndTimeMs();
+ }
+ }
+ return earliest;
+ }
+
+ /**
+ * A listener which is notified the initialization of schedule manager.
+ */
+ public interface OnInitializeListener {
+ /**
+ * Called when the schedule manager has been initialized.
+ */
+ void onInitialize();
}
/**
@@ -714,4 +977,4 @@ public class DvrScheduleManager {
*/
void onConflictStateChange(boolean conflict, ScheduledRecording... schedules);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
new file mode 100644
index 00000000..a653b5f4
--- /dev/null
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Environment;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.StatFs;
+import android.support.annotation.AnyThread;
+import android.support.annotation.IntDef;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.util.Utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Signals DVR storage status change such as plugging/unplugging.
+ */
+public class DvrStorageStatusManager {
+ private static final String TAG = "DvrStorageStatusManager";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Minimum storage size to support DVR
+ */
+ public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB
+ private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES
+ = 10 * 1024 * 1024 * 1024L; // 10GB
+ private static final String RECORDING_DATA_SUB_PATH = "/recording";
+
+ private static final String[] PROJECTION = {
+ TvContract.RecordedPrograms._ID,
+ TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
+ };
+ private final static int BATCH_OPERATION_COUNT = 100;
+
+ @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL,
+ STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StorageStatus {
+ }
+
+ /**
+ * Current storage is OK to record a program.
+ */
+ public static final int STORAGE_STATUS_OK = 0;
+
+ /**
+ * Current storage's total capacity is smaller than DVR requirement.
+ */
+ public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1;
+
+ /**
+ * Current storage's free space is insufficient to record programs.
+ */
+ public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2;
+
+ /**
+ * Current storage is missing.
+ */
+ public static final int STORAGE_STATUS_MISSING = 3;
+
+ private final Context mContext;
+ private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners =
+ new CopyOnWriteArraySet<>();
+ private final boolean mRunningInMainProcess;
+ private MountedStorageStatus mMountedStorageStatus;
+ private boolean mStorageValid;
+ private CleanUpDbTask mCleanUpDbTask;
+
+ private class MountedStorageStatus {
+ private final boolean mStorageMounted;
+ private final File mStorageMountedDir;
+ private final long mStorageMountedCapacity;
+
+ private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) {
+ mStorageMounted = mounted;
+ mStorageMountedDir = mountedDir;
+ mStorageMountedCapacity = capacity;
+ }
+
+ private boolean isValidForDvr() {
+ return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MountedStorageStatus)) {
+ return false;
+ }
+ MountedStorageStatus status = (MountedStorageStatus) other;
+ return mStorageMounted == status.mStorageMounted
+ && Objects.equals(mStorageMountedDir, status.mStorageMountedDir)
+ && mStorageMountedCapacity == status.mStorageMountedCapacity;
+ }
+ }
+
+ public interface OnStorageMountChangedListener {
+
+ /**
+ * Listener for DVR storage status change.
+ *
+ * @param storageMounted {@code true} when DVR possible storage is mounted,
+ * {@code false} otherwise.
+ */
+ void onStorageMountChanged(boolean storageMounted);
+ }
+
+ private final class StorageStatusBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ MountedStorageStatus result = getStorageStatusInternal();
+ if (mMountedStorageStatus.equals(result)) {
+ return;
+ }
+ mMountedStorageStatus = result;
+ if (result.mStorageMounted && mRunningInMainProcess) {
+ // Cleans up DB in LC process.
+ // Tuner process is not always on.
+ if (mCleanUpDbTask != null) {
+ mCleanUpDbTask.cancel(true);
+ }
+ mCleanUpDbTask = new CleanUpDbTask();
+ mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ boolean valid = result.isValidForDvr();
+ if (valid == mStorageValid) {
+ return;
+ }
+ mStorageValid = valid;
+ for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) {
+ l.onStorageMountChanged(valid);
+ }
+ }
+ }
+
+ /**
+ * Creates DvrStorageStatusManager.
+ *
+ * @param context {@link Context}
+ */
+ public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) {
+ mContext = context;
+ mRunningInMainProcess = runningInMainProcess;
+ mMountedStorageStatus = getStorageStatusInternal();
+ mStorageValid = mMountedStorageStatus.isValidForDvr();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_MEDIA_MOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ filter.addAction(Intent.ACTION_MEDIA_EJECT);
+ filter.addAction(Intent.ACTION_MEDIA_REMOVED);
+ filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
+ filter.addDataScheme(ContentResolver.SCHEME_FILE);
+ mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter);
+ }
+
+ /**
+ * Adds the listener for receiving storage status change.
+ *
+ * @param listener
+ */
+ public void addListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.add(listener);
+ }
+
+ /**
+ * Removes the current listener.
+ */
+ public void removeListener(OnStorageMountChangedListener listener) {
+ mOnStorageMountChangedListeners.remove(listener);
+ }
+
+ /**
+ * Returns true if a storage is mounted.
+ */
+ public boolean isStorageMounted() {
+ return mMountedStorageStatus.mStorageMounted;
+ }
+
+ /**
+ * Returns the path to DVR recording data directory.
+ * This can take for a while sometimes.
+ */
+ @WorkerThread
+ public File getRecordingRootDataDirectory() {
+ SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper());
+ if (mMountedStorageStatus.mStorageMountedDir == null) {
+ return null;
+ }
+ File root = mContext.getExternalFilesDir(null);
+ String rootPath;
+ try {
+ rootPath = root != null ? root.getCanonicalPath() : null;
+ } catch (IOException | SecurityException e) {
+ return null;
+ }
+ return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH);
+ }
+
+ /**
+ * Returns the current storage status for DVR recordings.
+ *
+ * @return {@link StorageStatus}
+ */
+ @AnyThread
+ public @StorageStatus int getDvrStorageStatus() {
+ MountedStorageStatus status = mMountedStorageStatus;
+ if (status.mStorageMountedDir == null) {
+ return STORAGE_STATUS_MISSING;
+ }
+ if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) {
+ return STORAGE_STATUS_OK;
+ }
+ if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL;
+ }
+ try {
+ StatFs statFs = new StatFs(status.mStorageMountedDir.toString());
+ if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ } catch (IllegalArgumentException e) {
+ // In rare cases, storage status change was not notified yet.
+ SoftPreconditions.checkState(false);
+ return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT;
+ }
+ return STORAGE_STATUS_OK;
+ }
+
+ /**
+ * Returns whether the storage has sufficient storage.
+ *
+ * @return {@code true} when there is sufficient storage, {@code false} otherwise
+ */
+ public boolean isStorageSufficient() {
+ return getDvrStorageStatus() == STORAGE_STATUS_OK;
+ }
+
+ private MountedStorageStatus getStorageStatusInternal() {
+ boolean storageMounted =
+ Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
+ File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null;
+ storageMounted = storageMounted && storageMountedDir != null;
+ long storageMountedCapacity = 0L;
+ if (storageMounted) {
+ try {
+ StatFs statFs = new StatFs(storageMountedDir.toString());
+ storageMountedCapacity = statFs.getTotalBytes();
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Storage mount status was changed.");
+ storageMounted = false;
+ storageMountedDir = null;
+ }
+ }
+ return new MountedStorageStatus(
+ storageMounted, storageMountedDir, storageMountedCapacity);
+ }
+
+ private class CleanUpDbTask extends AsyncTask<Void, Void, Void> {
+ private final ContentResolver mContentResolver;
+
+ private CleanUpDbTask() {
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus();
+ if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
+ return null;
+ }
+ List<ContentProviderOperation> ops = getDeleteOps(storageStatus
+ == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL);
+ if (ops == null || ops.isEmpty()) {
+ return null;
+ }
+ Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : "
+ + ops.size());
+ for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) {
+ int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size()
+ ? ops.size() : (i + BATCH_OPERATION_COUNT);
+ ArrayList<ContentProviderOperation> batchOps =
+ new ArrayList<>(ops.subList(i, toIndex));
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to clean up RecordedPrograms.", e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ if (mCleanUpDbTask == this) {
+ mCleanUpDbTask = null;
+ }
+ }
+
+ private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) {
+ List<ContentProviderOperation> ops = new ArrayList<>();
+
+ try (Cursor c = mContentResolver.query(
+ TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) {
+ if (c == null) {
+ return null;
+ }
+ while (c.moveToNext()) {
+ @DvrStorageStatusManager.StorageStatus int storageStatus =
+ getDvrStorageStatus();
+ if (isCancelled()
+ || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
+ ops.clear();
+ break;
+ }
+ String id = c.getString(0);
+ String packageName = c.getString(1);
+ String dataUriString = c.getString(2);
+ if (dataUriString == null) {
+ continue;
+ }
+ Uri dataUri = Uri.parse(dataUriString);
+ if (!Utils.isInBundledPackageSet(packageName)
+ || dataUri == null || dataUri.getPath() == null
+ || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
+ continue;
+ }
+ File recordedProgramDir = new File(dataUri.getPath());
+ if (deleteAll || !recordedProgramDir.exists()) {
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildRecordedProgramUri(Long.parseLong(id))).build());
+ }
+ }
+ return ops;
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java
index be934fd4..c0d3b0c5 100644
--- a/src/com/android/tv/dvr/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/DvrUiHelper.java
@@ -36,7 +36,6 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
-import com.android.tv.dvr.ui.DvrCancelAllSeriesRecordingDialogFragment;
import com.android.tv.dvr.ui.DvrDetailsActivity;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment;
@@ -44,13 +43,19 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialo
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
-import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrSchedulesActivity;
import com.android.tv.dvr.ui.DvrSeriesDeletionActivity;
+import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity;
import com.android.tv.dvr.ui.DvrSeriesSettingsActivity;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment;
+import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import com.android.tv.util.Utils;
@@ -82,7 +87,7 @@ public class DvrUiHelper {
}
} else {
SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
- if (seriesRecording == null) {
+ if (seriesRecording == null || seriesRecording.isStopped()) {
DvrUiHelper.showScheduleDialog(activity, program);
return false;
} else {
@@ -111,6 +116,32 @@ public class DvrUiHelper {
}
/**
+ * Checks if the storage status is good for recording and shows error messages if needed.
+ *
+ * @return true if the storage status is fine to be recorded for {@code inputId}.
+ */
+ public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) {
+ if (!Utils.isBundledInput(inputId)) {
+ return true;
+ }
+ DvrStorageStatusManager dvrStorageStatusManager =
+ TvApplication.getSingletons(activity).getDvrStorageStatusManager();
+ int status = dvrStorageStatusManager.getDvrStorageStatus();
+ if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
+ showDvrSmallSizedStorageErrorDialog(activity);
+ return false;
+ } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
+ showDvrMissingStorageErrorDialog(activity, inputId);
+ return false;
+ } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) {
+ // TODO: handle insufficient storage case.
+ return true;
+ } else {
+ return true;
+ }
+ }
+
+ /**
* Shows the schedule dialog.
*/
public static void showScheduleDialog(MainActivity activity, Program program) {
@@ -170,7 +201,7 @@ public class DvrUiHelper {
/**
* Shows DVR missing storage error dialog.
*/
- public static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) {
+ private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) {
SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId));
Bundle args = new Bundle();
args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId);
@@ -178,15 +209,23 @@ public class DvrUiHelper {
}
/**
+ * Shows DVR small sized storage error dialog.
+ */
+ public static void showDvrSmallSizedStorageErrorDialog(Activity activity) {
+ showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null);
+ }
+
+ /**
* Shows stop recording dialog.
*/
- public static void showStopRecordingDialog(MainActivity activity, Channel channel) {
- if (channel == null) {
- return;
- }
+ public static void showStopRecordingDialog(Activity activity, long channelId, int reason,
+ HalfSizedDialogFragment.OnActionClickListener listener) {
Bundle args = new Bundle();
- args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
- showDialogFragment(activity, new DvrStopRecordingDialogFragment(), args);
+ args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId);
+ args.putInt(DvrStopRecordingFragment.KEY_REASON, reason);
+ DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment();
+ fragment.setOnActionClickListener(listener);
+ showDialogFragment(activity, fragment, args);
}
/**
@@ -244,7 +283,8 @@ public class DvrUiHelper {
recordings) {
ScheduledRecording earlistScheduledRecording = null;
if (!recordings.isEmpty()) {
- Collections.sort(recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ Collections.sort(recordings,
+ ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
earlistScheduledRecording = recordings.get(0);
}
return earlistScheduledRecording;
@@ -300,32 +340,72 @@ public class DvrUiHelper {
/**
* Shows the series settings activity.
+ *
+ * @param channelIds Channel ID list which has programs belonging to the series.
*/
- public static void startSeriesSettingsActivity(Context context, long seriesRecordingId) {
+ public static void startSeriesSettingsActivity(Context context, long seriesRecordingId,
+ @Nullable long[] channelIds, boolean removeEmptySeriesSchedule,
+ boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) {
Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId);
+ intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds);
+ intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING,
+ removeEmptySeriesSchedule);
+ intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent);
+ intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG,
+ showViewScheduleOptionInDialog);
context.startActivity(intent);
}
/**
- * Shows the details activity for the schedule.
+ * Shows "series recording scheduled" dialog activity.
*/
- public static void startDetailsActivity(Activity activity, ScheduledRecording schedule,
+ public static void StartSeriesScheduledDialogActivity(Context context,
+ SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) {
+ if (seriesRecording == null) {
+ return;
+ }
+ Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class);
+ intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID,
+ seriesRecording.getId());
+ intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION,
+ showViewScheduleOptionInDialog);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Shows the details activity for the DVR items. The type of DVR items may be
+ * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}.
+ */
+ public static void startDetailsActivity(Activity activity, Object dvrItem,
@Nullable ImageView imageView, boolean hideViewSchedule) {
- if (schedule == null) {
+ if (dvrItem == null) {
return;
}
+ Intent intent = new Intent(activity, DvrDetailsActivity.class);
+ long recordingId;
int viewType;
- if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
- viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
- } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+ if (dvrItem instanceof ScheduledRecording) {
+ ScheduledRecording schedule = (ScheduledRecording) dvrItem;
+ recordingId = schedule.getId();
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+ } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+ } else {
+ return;
+ }
+ } else if (dvrItem instanceof RecordedProgram) {
+ recordingId = ((RecordedProgram) dvrItem).getId();
+ viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ } else if (dvrItem instanceof SeriesRecording) {
+ recordingId = ((SeriesRecording) dvrItem).getId();
+ viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW;
} else {
return;
}
- Intent intent = new Intent(activity, DvrDetailsActivity.class);
+ intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId);
intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType);
- intent.putExtra(DvrDetailsActivity.RECORDING_ID, schedule.getId());
intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
Bundle bundle = null;
if (imageView != null) {
@@ -336,30 +416,18 @@ public class DvrUiHelper {
}
/**
- * Shows the details activity for the recorded program.
- */
- public static void startDetailsActivity(Activity activity, RecordedProgram recordedProgram,
- @Nullable ImageView imageView) {
- Intent intent = new Intent(activity, DvrDetailsActivity.class);
- intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordedProgram.getId());
- intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE,
- DvrDetailsActivity.RECORDED_PROGRAM_VIEW);
- Bundle bundle = null;
- if (imageView != null) {
- bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView,
- DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
- }
- activity.startActivity(intent, bundle);
- }
-
- /**
* Shows the cancel all dialog for series schedules list.
*/
- public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity) {
- DvrCancelAllSeriesRecordingDialogFragment dvrCancelAllSeriesRecordingDialogFragment =
- new DvrCancelAllSeriesRecordingDialogFragment();
- dvrCancelAllSeriesRecordingDialogFragment.show(activity.getFragmentManager(),
- DvrCancelAllSeriesRecordingDialogFragment.DIALOG_TAG);
+ public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity,
+ SeriesRecording seriesRecording) {
+ DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment =
+ new DvrStopSeriesRecordingDialogFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING,
+ seriesRecording);
+ dvrStopSeriesRecordingDialogFragment.setArguments(arguments);
+ dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(),
+ DvrStopSeriesRecordingDialogFragment.DIALOG_TAG);
}
/**
diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
index cb723f83..4eada742 100644
--- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java
+++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
@@ -19,9 +19,12 @@ package com.android.tv.dvr;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.tv.TvInputManager;
+import android.support.annotation.IntDef;
import com.android.tv.common.SharedPreferencesUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -37,13 +40,33 @@ public class DvrWatchedPositionManager {
private final boolean DEBUG = false;
private SharedPreferences mWatchedPositions;
- private final Context mContext;
private final Map<Long, Set> mListeners = new HashMap<>();
+ /**
+ * The minimum percentage of recorded program being watched that will be considered as being
+ * completely watched.
+ */
+ public static final float DVR_WATCHED_THRESHOLD_RATE = 0.98f;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({DVR_WATCHED_STATUS_NEW, DVR_WATCHED_STATUS_WATCHING, DVR_WATCHED_STATUS_WATCHED})
+ public @interface DvrWatchedStatus {}
+ /**
+ * The status indicates the recorded program has not been watched at all.
+ */
+ public static final int DVR_WATCHED_STATUS_NEW = 0;
+ /**
+ * The status indicates the recorded program is being watched.
+ */
+ public static final int DVR_WATCHED_STATUS_WATCHING = 1;
+ /**
+ * The status indicates the recorded program was completely watched.
+ */
+ public static final int DVR_WATCHED_STATUS_WATCHED = 2;
+
public DvrWatchedPositionManager(Context context) {
- mContext = context.getApplicationContext();
- mWatchedPositions = mContext.getSharedPreferences(SharedPreferencesUtils
- .SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE);
+ mWatchedPositions = context.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE);
}
/**
@@ -62,6 +85,18 @@ public class DvrWatchedPositionManager {
TvInputManager.TIME_SHIFT_INVALID_TIME);
}
+ @DvrWatchedStatus public int getWatchedStatus(RecordedProgram recordedProgram) {
+ long watchedPosition = getWatchedPosition(recordedProgram.getId());
+ if (watchedPosition == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ return DVR_WATCHED_STATUS_NEW;
+ } else if (watchedPosition > recordedProgram
+ .getDurationMillis() * DVR_WATCHED_THRESHOLD_RATE) {
+ return DVR_WATCHED_STATUS_WATCHED;
+ } else {
+ return DVR_WATCHED_STATUS_WATCHING;
+ }
+ }
+
/**
* Adds {@link WatchedPositionChangedListener}.
*/
diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java
new file mode 100644
index 00000000..15ca2700
--- /dev/null
+++ b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Program;
+import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
+import com.android.tv.util.AsyncDbTask.CursorFilter;
+import com.android.tv.util.PermissionUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+abstract public class EpisodicProgramLoadTask {
+ private static final String TAG = "EpisodicProgramLoadTask";
+
+ private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID);
+ private static final int START_TIME_INDEX =
+ Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS);
+ private static final int RECORDING_PROHIBITED_INDEX =
+ Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED);
+
+ private static final String PARAM_START_TIME = "start_time";
+ private static final String PARAM_END_TIME = "end_time";
+
+ private static final String PROGRAM_PREDICATE =
+ Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND "
+ + Programs.COLUMN_RECORDING_PROHIBITED + "=0";
+ private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM =
+ Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND "
+ + Programs.COLUMN_RECORDING_PROHIBITED + "=0";
+ private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?";
+ private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?";
+
+ private final Context mContext;
+ private final DvrDataManager mDataManager;
+ private boolean mQueryAllChannels;
+ private boolean mLoadCurrentProgram;
+ private boolean mLoadScheduledEpisode;
+ private boolean mLoadDisallowedProgram;
+ // If true, match programs with OPTION_CHANNEL_ALL.
+ private boolean mIgnoreChannelOption;
+ private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>();
+ private AsyncProgramQueryTask mProgramQueryTask;
+
+ /**
+ *
+ * Constructor used to load programs for one series recording with the given channel option.
+ */
+ public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) {
+ this(context, Collections.singletonList(seriesRecording));
+ }
+
+ /**
+ * Constructor used to load programs for multiple series recordings. The channel option is
+ * {@link SeriesRecording#OPTION_CHANNEL_ALL}.
+ */
+ public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) {
+ mContext = context.getApplicationContext();
+ mDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mSeriesRecordings.addAll(seriesRecordings);
+ }
+
+ /**
+ * Returns the series recordings.
+ */
+ public List<SeriesRecording> getSeriesRecordings() {
+ return mSeriesRecordings;
+ }
+
+ /**
+ * Returns the program query task. It is {@code null} until it is executed.
+ */
+ @Nullable
+ public AsyncProgramQueryTask getTask() {
+ return mProgramQueryTask;
+ }
+
+ /**
+ * Enables loading current programs. The default value is {@code false}.
+ */
+ public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) {
+ SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
+ "Can't change setting after execution.");
+ mLoadCurrentProgram = loadCurrentProgram;
+ return this;
+ }
+
+ /**
+ * Enables already schedules episodes. The default value is {@code false}.
+ */
+ public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) {
+ SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
+ "Can't change setting after execution.");
+ mLoadScheduledEpisode = loadScheduledEpisode;
+ return this;
+ }
+
+ /**
+ * Enables loading disallowed programs whose schedules were removed manually by the user.
+ * The default value is {@code false}.
+ */
+ public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) {
+ SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
+ "Can't change setting after execution.");
+ mLoadDisallowedProgram = loadDisallowedProgram;
+ return this;
+ }
+
+ /**
+ * Gives the option whether to ignore the channel option when matching programs.
+ * If {@code ignoreChannelOption} is {@code true}, the program will be matched with
+ * {@link SeriesRecording#OPTION_CHANNEL_ALL} option.
+ */
+ public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) {
+ SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
+ "Can't change setting after execution.");
+ mIgnoreChannelOption = ignoreChannelOption;
+ return this;
+ }
+
+ /**
+ * Executes the task.
+ *
+ * @see com.android.tv.util.AsyncDbTask#executeOnDbThread
+ */
+ public void execute() {
+ if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
+ "Can't execute task: the task is already running.")) {
+ mQueryAllChannels = mSeriesRecordings.size() > 1
+ || mSeriesRecordings.get(0).getChannelOption()
+ == SeriesRecording.OPTION_CHANNEL_ALL
+ || mIgnoreChannelOption;
+ mProgramQueryTask = createTask();
+ mProgramQueryTask.executeOnDbThread();
+ }
+ }
+
+ /**
+ * Cancels the task.
+ *
+ * @see android.os.AsyncTask#cancel
+ */
+ public void cancel(boolean mayInterruptIfRunning) {
+ if (mProgramQueryTask != null) {
+ mProgramQueryTask.cancel(mayInterruptIfRunning);
+ }
+ }
+
+ /**
+ * Runs on the UI thread after the program loading finishes successfully.
+ */
+ protected void onPostExecute(List<Program> programs) {
+ }
+
+ /**
+ * Runs on the UI thread after the program loading was canceled.
+ */
+ protected void onCancelled(List<Program> programs) {
+ }
+
+ private AsyncProgramQueryTask createTask() {
+ SqlParams sqlParams = createSqlParams();
+ return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri,
+ sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ EpisodicProgramLoadTask.this.onPostExecute(programs);
+ }
+
+ @Override
+ protected void onCancelled(List<Program> programs) {
+ EpisodicProgramLoadTask.this.onCancelled(programs);
+ }
+ };
+ }
+
+ private SqlParams createSqlParams() {
+ SqlParams sqlParams = new SqlParams();
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ sqlParams.uri = Programs.CONTENT_URI;
+ // Base
+ StringBuilder selection = new StringBuilder(mLoadCurrentProgram
+ ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE);
+ List<String> args = new ArrayList<>();
+ args.add(Long.toString(System.currentTimeMillis()));
+ // Channel option
+ if (!mQueryAllChannels) {
+ selection.append(" AND ").append(CHANNEL_ID_PREDICATE);
+ args.add(Long.toString(mSeriesRecordings.get(0).getChannelId()));
+ }
+ // Title
+ if (mSeriesRecordings.size() == 1) {
+ selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE);
+ args.add(mSeriesRecordings.get(0).getTitle());
+ }
+ sqlParams.selection = selection.toString();
+ sqlParams.selectionArgs = args.toArray(new String[args.size()]);
+ sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings);
+ } else {
+ // The query includes the current program. Will be filtered if needed.
+ if (mQueryAllChannels) {
+ sqlParams.uri = Programs.CONTENT_URI.buildUpon()
+ .appendQueryParameter(PARAM_START_TIME,
+ String.valueOf(System.currentTimeMillis()))
+ .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE))
+ .build();
+ } else {
+ sqlParams.uri = TvContract.buildProgramsUriForChannel(
+ mSeriesRecordings.get(0).getChannelId(),
+ System.currentTimeMillis(), Long.MAX_VALUE);
+ }
+ sqlParams.selection = null;
+ sqlParams.selectionArgs = null;
+ sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings);
+ }
+ return sqlParams;
+ }
+
+ @VisibleForTesting
+ static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes,
+ ScheduledEpisode episode) {
+ // The episode whose season number or episode number is null will always be scheduled.
+ return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber)
+ && !TextUtils.isEmpty(episode.episodeNumber);
+ }
+
+ /**
+ * Filter the programs which match the series recording. The episodes which the schedules are
+ * already created for are filtered out too.
+ */
+ private class SeriesRecordingCursorFilter implements CursorFilter {
+ private final Set<Long> mDisallowedProgramIds = new HashSet<>();
+ private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>();
+
+ SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) {
+ if (!mLoadDisallowedProgram) {
+ mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds());
+ }
+ if (!mLoadScheduledEpisode) {
+ Set<Long> seriesRecordingIds = new HashSet<>();
+ for (SeriesRecording r : seriesRecordings) {
+ seriesRecordingIds.add(r.getId());
+ }
+ for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
+ if (seriesRecordingIds.contains(r.getSeriesRecordingId())
+ && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
+ && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
+ mScheduledEpisodes.add(new ScheduledEpisode(r));
+ }
+ }
+ }
+ }
+
+ @Override
+ @WorkerThread
+ public boolean filter(Cursor c) {
+ if (!mLoadDisallowedProgram
+ && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
+ return false;
+ }
+ Program program = Program.fromCursor(c);
+ for (SeriesRecording seriesRecording : mSeriesRecordings) {
+ boolean programMatches;
+ if (mIgnoreChannelOption) {
+ programMatches = seriesRecording.matchProgram(program,
+ SeriesRecording.OPTION_CHANNEL_ALL);
+ } else {
+ programMatches = seriesRecording.matchProgram(program);
+ }
+ if (programMatches) {
+ return mLoadScheduledEpisode
+ || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode(
+ seriesRecording.getId(), program.getSeasonNumber(),
+ program.getEpisodeNumber()));
+ }
+ }
+ return false;
+ }
+ }
+
+ private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter {
+ SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings) {
+ super(seriesRecordings);
+ }
+
+ @Override
+ public boolean filter(Cursor c) {
+ return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
+ && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c);
+ }
+ }
+
+ private static class SqlParams {
+ public Uri uri;
+ public String selection;
+ public String[] selectionArgs;
+ public CursorFilter filter;
+ }
+
+ /**
+ * A plain java object which includes the season/episode number for the series recording.
+ */
+ public static class ScheduledEpisode {
+ public final long seriesRecordingId;
+ public final String seasonNumber;
+ public final String episodeNumber;
+
+ /**
+ * Create a new Builder with the values set from an existing {@link ScheduledRecording}.
+ */
+ ScheduledEpisode(ScheduledRecording r) {
+ this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
+ }
+
+ public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) {
+ this.seriesRecordingId = seriesRecordingId;
+ this.seasonNumber = seasonNumber;
+ this.episodeNumber = episodeNumber;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ScheduledEpisode)) return false;
+ ScheduledEpisode that = (ScheduledEpisode) o;
+ return seriesRecordingId == that.seriesRecordingId
+ && Objects.equals(seasonNumber, that.seasonNumber)
+ && Objects.equals(episodeNumber, that.episodeNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
+ }
+
+ @Override
+ public String toString() {
+ return "ScheduledEpisode{" +
+ "seriesRecordingId=" + seriesRecordingId +
+ ", seasonNumber='" + seasonNumber +
+ ", episodeNumber=" + episodeNumber +
+ '}';
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java
index 23eacb73..53c89ebc 100644
--- a/src/com/android/tv/dvr/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/InputTaskScheduler.java
@@ -21,7 +21,6 @@ import android.media.tv.TvInputInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.ArrayMap;
@@ -32,9 +31,11 @@ import com.android.tv.InputSessionManager;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.util.Clock;
+import com.android.tv.util.CompositeComparator;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -42,7 +43,6 @@ import java.util.Map;
/**
* The scheduler for a TV input.
*/
-@MainThread
public class InputTaskScheduler {
private static final String TAG = "InputTaskScheduler";
private static final boolean DEBUG = false;
@@ -51,6 +51,24 @@ public class InputTaskScheduler {
private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2;
private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3;
private static final int MSG_BUILD_SCHEDULE = 4;
+ private static final int MSG_STOP_SCHEDULE = 5;
+
+ private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f;
+
+ // The candidate comparator should be the consistent with
+ // DvrScheduleManager#CANDIDATE_COMPARATOR.
+ private static final Comparator<RecordingTask> CANDIDATE_COMPARATOR =
+ new CompositeComparator<>(
+ RecordingTask.PRIORITY_COMPARATOR,
+ RecordingTask.END_TIME_COMPARATOR,
+ RecordingTask.ID_COMPARATOR);
+
+ /**
+ * Returns the comparator which the schedules are sorted with when executed.
+ */
+ public static Comparator<ScheduledRecording> getRecordingOrderComparator() {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR;
+ }
/**
* Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done.
@@ -217,6 +235,23 @@ public class InputTaskScheduler {
}
}
+ /**
+ * Stops the input task scheduler.
+ */
+ public void stop() {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE);
+ }
+
+ private void handleStopSchedule() {
+ mWaitingSchedules.clear();
+ int size = mPendingRecordings.size();
+ for (int i = 0; i < size; ++i) {
+ RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
+ task.cleanUp();
+ }
+ }
+
@VisibleForTesting
void handleBuildSchedule() {
if (mWaitingSchedules.isEmpty()) {
@@ -227,7 +262,8 @@ public class InputTaskScheduler {
for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator();
iter.hasNext(); ) {
ScheduledRecording schedule = iter.next();
- if (schedule.getEndTimeMs() <= currentTimeMs) {
+ if (schedule.getEndTimeMs() - currentTimeMs
+ <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
fail(schedule);
iter.remove();
}
@@ -244,7 +280,13 @@ public class InputTaskScheduler {
schedulesToStart.add(schedule);
}
}
- Collections.sort(schedulesToStart, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ // The schedules will be executed with the following order.
+ // 1. The schedule which starts early. It can be replaced later when the schedule with the
+ // higher priority needs to start.
+ // 2. The schedule with the higher priority. It can be replaced later when the schedule with
+ // the higher priority needs to start.
+ // 3. The schedule which was created recently.
+ Collections.sort(schedulesToStart, getRecordingOrderComparator());
int tunerCount;
synchronized (mInputLock) {
tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0;
@@ -266,11 +308,6 @@ public class InputTaskScheduler {
task.stop();
// Just return. The schedules will be rebuilt after the task is stopped.
return;
- } else {
- // TODO: Do not fail immediately. Start the recording later when available.
- // There are no replaceable task. Remove it.
- fail(schedule);
- mWaitingSchedules.remove(schedule.getId());
}
}
}
@@ -280,12 +317,20 @@ public class InputTaskScheduler {
// Set next scheduling.
long earliest = Long.MAX_VALUE;
for (ScheduledRecording schedule : mWaitingSchedules.values()) {
- if (earliest > schedule.getStartTimeMs()) {
- earliest = schedule.getStartTimeMs();
+ // The conflicting schedules will be removed if they end before conflicting resolved.
+ if (schedulesToStart.contains(schedule)) {
+ if (earliest > schedule.getEndTimeMs()) {
+ earliest = schedule.getEndTimeMs();
+ }
+ } else {
+ if (earliest > schedule.getStartTimeMs()
+ - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) {
+ earliest = schedule.getStartTimeMs()
+ - RecordingTask.RECORDING_EARLY_START_OFFSET_MS;
+ }
}
}
- mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest
- - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - currentTimeMs);
+ mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs);
}
private RecordingTask createRecordingTask(ScheduledRecording schedule) {
@@ -309,13 +354,18 @@ public class InputTaskScheduler {
}
private RecordingTask getReplacableTask(ScheduledRecording schedule) {
+ // Returns the recording with the following priority.
+ // 1. The recording with the lowest priority is returned.
+ // 2. If the priorities are the same, the recording which finishes early is returned.
+ // 3. If 1) and 2) are the same, the early created schedule is returned.
int size = mPendingRecordings.size();
RecordingTask candidate = null;
for (int i = 0; i < size; ++i) {
RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask;
- if (schedule.getPriority() > task.getPriority()
- && (candidate == null || candidate.getPriority() > task.getPriority())) {
- candidate = task;
+ if (schedule.getPriority() > task.getPriority()) {
+ if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) {
+ candidate = task;
+ }
}
}
return candidate;
@@ -372,6 +422,9 @@ public class InputTaskScheduler {
case MSG_BUILD_SCHEDULE:
handleBuildSchedule();
break;
+ case MSG_STOP_SCHEDULE:
+ handleStopSchedule();
+ break;
}
}
}
diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java
index 085402a4..dd744f80 100644
--- a/src/com/android/tv/dvr/RecordedProgram.java
+++ b/src/com/android/tv/dvr/RecordedProgram.java
@@ -18,21 +18,25 @@ package com.android.tv.dvr;
import static android.media.tv.TvContract.RecordedPrograms;
+import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.net.Uri;
+import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.android.tv.common.R;
import com.android.tv.data.BaseProgram;
+import com.android.tv.data.GenreItems;
import com.android.tv.data.InternalDataUtils;
import com.android.tv.util.Utils;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -40,6 +44,7 @@ import java.util.concurrent.TimeUnit;
/**
* Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}.
*/
+@TargetApi(Build.VERSION_CODES.N)
public class RecordedProgram extends BaseProgram {
public static final int ID_NOT_SET = -1;
@@ -553,6 +558,21 @@ public class RecordedProgram extends BaseProgram {
return mCanonicalGenres;
}
+ /**
+ * Returns array of canonical genre ID's for this recorded program.
+ */
+ @Override
+ public int[] getCanonicalGenreIds() {
+ if (mCanonicalGenres == null) {
+ return null;
+ }
+ int[] genreIds = new int[mCanonicalGenres.length];
+ for (int i = 0; i < mCanonicalGenres.length; i++) {
+ genreIds[i] = GenreItems.getId(mCanonicalGenres[i]);
+ }
+ return genreIds;
+ }
+
@Override
public long getChannelId() {
return mChannelId;
@@ -622,6 +642,21 @@ public class RecordedProgram extends BaseProgram {
}
}
+ @Nullable
+ public String getEpisodeDisplayNumber(Context context) {
+ if (!TextUtils.isEmpty(mEpisodeNumber)) {
+ if (TextUtils.equals(mSeasonNumber, "0")) {
+ // Do not show "S0: ".
+ return String.format(context.getResources().getString(
+ R.string.display_episode_number_format_no_season_number), mEpisodeNumber);
+ } else {
+ return String.format(context.getResources().getString(
+ R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber);
+ }
+ }
+ return null;
+ }
+
public long getExpireTimeUtcMillis() {
return mExpireTimeUtcMillis;
}
@@ -678,6 +713,7 @@ public class RecordedProgram extends BaseProgram {
return mSearchable;
}
+ @Override
public String getSeriesId() {
return mSeriesId;
}
@@ -822,4 +858,11 @@ public class RecordedProgram extends BaseProgram {
private static String safeEncode(@Nullable String[] genres) {
return genres == null ? null : TvContract.Programs.Genres.encode(genres);
}
+
+ /**
+ * Returns an array containing all of the elements in the list.
+ */
+ public static RecordedProgram[] toArray(Collection<RecordedProgram> recordedPrograms) {
+ return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]);
+ }
}
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java
index 2373f15c..c3d236b0 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/RecordingTask.java
@@ -41,6 +41,7 @@ import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper;
import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
+import java.util.Comparator;
import java.util.concurrent.TimeUnit;
/**
@@ -57,6 +58,39 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
private static final String TAG = "RecordingTask";
private static final boolean DEBUG = false;
+ /**
+ * Compares the end time in ascending order.
+ */
+ public static final Comparator<RecordingTask> END_TIME_COMPARATOR
+ = new Comparator<RecordingTask>() {
+ @Override
+ public int compare(RecordingTask lhs, RecordingTask rhs) {
+ return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs());
+ }
+ };
+
+ /**
+ * Compares ID in ascending order.
+ */
+ public static final Comparator<RecordingTask> ID_COMPARATOR
+ = new Comparator<RecordingTask>() {
+ @Override
+ public int compare(RecordingTask lhs, RecordingTask rhs) {
+ return Long.compare(lhs.getScheduleId(), rhs.getScheduleId());
+ }
+ };
+
+ /**
+ * Compares the priority in ascending order.
+ */
+ public static final Comparator<RecordingTask> PRIORITY_COMPARATOR
+ = new Comparator<RecordingTask>() {
+ @Override
+ public int compare(RecordingTask lhs, RecordingTask rhs) {
+ return Long.compare(lhs.getPriority(), rhs.getPriority());
+ }
+ };
+
@VisibleForTesting
static final int MSG_INITIALIZE = 1;
@VisibleForTesting
@@ -169,6 +203,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
}
@Override
+ public void onConnectionFailed(String inputId) {
+ if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")");
+ if (mRecordingSession != null) {
+ failAndQuit();
+ }
+ }
+
+ @Override
public void onTuned(Uri channelUri) {
if (DEBUG) Log.d(TAG, "onTuned");
if (mRecordingSession == null) {
@@ -252,7 +294,8 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
String inputId = mChannel.getInputId();
mRecordingSession = mSessionManager.createRecordingSession(inputId,
- "recordingTask-" + mScheduledRecording.getId(), this, mHandler);
+ "recordingTask-" + mScheduledRecording.getId(), this,
+ mHandler, mScheduledRecording.getEndTimeMs());
mState = State.SESSION_ACQUIRED;
mDvrManager.addListener(this, mHandler);
mRecordingSession.tune(inputId, mChannel.getUri());
@@ -302,11 +345,15 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
private void handleUpdateSchedule(ScheduledRecording schedule) {
mScheduledRecording = schedule;
// Check end time only. The start time is checked in InputTaskScheduler.
- if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()
- && mState == State.RECORDING_STARTED) {
- mHandler.removeMessages(MSG_STOP_RECORDING);
- if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) {
- failAndQuit();
+ if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) {
+ if (mRecordingSession != null) {
+ mRecordingSession.setEndTimeMs(schedule.getEndTimeMs());
+ }
+ if (mState == State.RECORDING_STARTED) {
+ mHandler.removeMessages(MSG_STOP_RECORDING);
+ if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) {
+ failAndQuit();
+ }
}
}
}
@@ -316,6 +363,10 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
return mState;
}
+ private long getScheduleId() {
+ return mScheduledRecording.getId();
+ }
+
/**
* Returns the priority.
*/
@@ -359,7 +410,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state);
mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state)
.build();
- mMainThreadHandler.post(new Runnable() {
+ runOnMainThread(new Runnable() {
@Override
public void run() {
ScheduledRecording schedule = mDataManager.getScheduledRecording(
@@ -429,6 +480,19 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
removeRecordedProgram();
}
+ /**
+ * Clean up the task.
+ */
+ public void cleanUp() {
+ if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) {
+ updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
+ }
+ release();
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
@Override
public String toString() {
return getClass().getName() + "(" + mScheduledRecording + ")";
diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java
index a9673b40..2bda10ea 100644
--- a/src/com/android/tv/dvr/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/ScheduledRecording.java
@@ -31,6 +31,7 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
import com.android.tv.dvr.provider.DvrContract.Schedules;
+import com.android.tv.util.CompositeComparator;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
@@ -56,6 +57,9 @@ public final class ScheduledRecording implements Parcelable {
*/
public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
+ /**
+ * Compares the start time in ascending order.
+ */
public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR
= new Comparator<ScheduledRecording>() {
@Override
@@ -65,7 +69,7 @@ public final class ScheduledRecording implements Parcelable {
};
/**
- * Compare the end time in ascending order.
+ * Compares the end time in ascending order.
*/
public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR
= new Comparator<ScheduledRecording>() {
@@ -76,34 +80,36 @@ public final class ScheduledRecording implements Parcelable {
};
/**
- * Compare priority in descending order.
+ * Compares ID in ascending order. The schedule with the larger ID was created later.
*/
- public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR
+ public static final Comparator<ScheduledRecording> ID_COMPARATOR
= new Comparator<ScheduledRecording>() {
@Override
public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- int value = Long.compare(rhs.mPriority, lhs.mPriority);
- if (value == 0) {
- // New recording has the higher priority.
- value = Long.compare(rhs.mId, lhs.mId);
- }
- return value;
+ return Long.compare(lhs.mId, rhs.mId);
}
};
- public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR
+ /**
+ * Compares the priority in ascending order.
+ */
+ public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR
= new Comparator<ScheduledRecording>() {
@Override
public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
- int value = START_TIME_COMPARATOR.compare(lhs, rhs);
- if (value == 0) {
- value = PRIORITY_COMPARATOR.compare(lhs, rhs);
- }
- return value;
+ return Long.compare(lhs.mPriority, rhs.mPriority);
}
};
/**
+ * Compares start time in ascending order and then priority in descending order and then ID in
+ * descending order.
+ */
+ public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+ = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(),
+ ID_COMPARATOR.reversed());
+
+ /**
* Builds scheduled recordings from programs.
*/
public static Builder builder(String inputId, Program p) {
@@ -285,6 +291,7 @@ public final class ScheduledRecording implements Parcelable {
.setChannelId(orig.mChannelId)
.setEndTimeMs(orig.mEndTimeMs)
.setSeriesRecordingId(orig.mSeriesRecordingId)
+ .setPriority(orig.mPriority)
.setProgramId(orig.mProgramId)
.setProgramTitle(orig.mProgramTitle)
.setStartTimeMs(orig.mStartTimeMs)
@@ -766,6 +773,13 @@ public final class ScheduledRecording implements Parcelable {
return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower();
}
+ /**
+ * Checks if the {@code schedule} overlaps with this schedule.
+ */
+ public boolean isOverLapping(ScheduledRecording schedule) {
+ return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs();
+ }
+
@Override
public String toString() {
return "ScheduledRecording[" + mId
@@ -775,8 +789,8 @@ public final class ScheduledRecording implements Parcelable {
+ ",programId=" + mProgramId
+ ",programTitle=" + mProgramTitle
+ ",type=" + mType
- + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
- + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs)
+ + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")"
+ + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")"
+ ",seasonNumber=" + mSeasonNumber
+ ",episodeNumber=" + mEpisodeNumber
+ ",episodeTitle=" + mEpisodeTitle
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java
index 25904ee4..ce78e1be 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/Scheduler.java
@@ -123,6 +123,9 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
* Stops the scheduler.
*/
public void stop() {
+ for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) {
+ inputTaskScheduler.stop();
+ }
mInputManager.removeCallback(this);
mDataManager.removeScheduledRecordingListener(this);
}
@@ -173,13 +176,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
}
boolean needToUpdateAlarm = false;
for (ScheduledRecording schedule : schedules) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId());
- if (input == null) {
- Log.e(TAG, "Can't find input for " + schedule);
- mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
- continue;
- }
- InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
+ InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
if (scheduler != null) {
scheduler.removeSchedule(schedule);
needToUpdateAlarm = true;
@@ -198,12 +195,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
}
// Update the recordings.
for (ScheduledRecording schedule : schedules) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId());
- if (input == null) {
- Log.e(TAG, "Can't find input for " + schedule);
- continue;
- }
- InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
+ InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
if (scheduler != null) {
scheduler.updateSchedule(schedule);
}
@@ -228,7 +220,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
}
private void scheduleRecordingSoon(ScheduledRecording schedule) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId());
+ TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
if (input == null) {
Log.e(TAG, "Can't find input for " + schedule);
mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
@@ -260,7 +252,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class);
PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
// This will cancel the previous alarm.
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
+ mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent);
} else {
if (DEBUG) Log.d(TAG, "No future recording, alarm not set");
}
diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java
index fc68eaf7..f0690f5f 100644
--- a/src/com/android/tv/dvr/SeriesRecording.java
+++ b/src/com/android/tv/dvr/SeriesRecording.java
@@ -21,10 +21,10 @@ import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import com.android.tv.data.BaseProgram;
import com.android.tv.data.Program;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.Utils;
@@ -69,8 +69,8 @@ public class SeriesRecording implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true,
- value = {STATE_SERIES_NORMAL, STATE_SERIES_CANCELED})
- private @interface SeriesState {}
+ value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED})
+ public @interface SeriesState {}
/**
* The state indicates that the series recording is a normal one.
@@ -78,9 +78,9 @@ public class SeriesRecording implements Parcelable {
public static final int STATE_SERIES_NORMAL = 0;
/**
- * The state indicates that the series recording is canceled.
+ * The state indicates that the series recording is stopped.
*/
- public static final int STATE_SERIES_CANCELED = 1;
+ public static final int STATE_SERIES_STOPPED = 1;
/**
* Compare priority in descending order.
@@ -110,9 +110,9 @@ public class SeriesRecording implements Parcelable {
};
/**
- * Creates a new Builder with the values set from the series information of {@link Program}.
+ * Creates a new Builder with the values set from the series information of {@link BaseProgram}.
*/
- public static Builder builder(String inputId, Program p) {
+ public static Builder builder(String inputId, BaseProgram p) {
return new Builder()
.setInputId(inputId)
.setSeriesId(p.getSeriesId())
@@ -190,7 +190,7 @@ public class SeriesRecording implements Parcelable {
.setCanonicalGenreIds(c.getString(++index))
.setPosterUri(c.getString(++index))
.setPhotoUri(c.getString(++index))
- .setState(seriesRecordingCanceled(c.getString(++index)))
+ .setState(seriesRecordingState(c.getString(++index)))
.build();
}
@@ -220,7 +220,7 @@ public class SeriesRecording implements Parcelable {
Utils.getCanonicalGenre(r.getCanonicalGenreIds()));
values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri());
values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri());
- values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingCanceled(r.getState()));
+ values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState()));
return values;
}
@@ -244,22 +244,22 @@ public class SeriesRecording implements Parcelable {
return OPTION_CHANNEL_ONE;
}
- private static String seriesRecordingCanceled(@SeriesState int state) {
+ private static String seriesRecordingState(@SeriesState int state) {
switch (state) {
case STATE_SERIES_NORMAL:
return SeriesRecordings.STATE_SERIES_NORMAL;
- case STATE_SERIES_CANCELED:
- return SeriesRecordings.STATE_SERIES_CANCELED;
+ case STATE_SERIES_STOPPED:
+ return SeriesRecordings.STATE_SERIES_STOPPED;
}
return SeriesRecordings.STATE_SERIES_NORMAL;
}
- @SeriesState private static int seriesRecordingCanceled(String state) {
+ @SeriesState private static int seriesRecordingState(String state) {
switch (state) {
case SeriesRecordings.STATE_SERIES_NORMAL:
return STATE_SERIES_NORMAL;
- case SeriesRecordings.STATE_SERIES_CANCELED:
- return STATE_SERIES_CANCELED;
+ case SeriesRecordings.STATE_SERIES_STOPPED:
+ return STATE_SERIES_STOPPED;
}
return STATE_SERIES_NORMAL;
}
@@ -331,6 +331,7 @@ public class SeriesRecording implements Parcelable {
mInputId = inputId;
return this;
}
+
/**
* @see #getChannelId()
*/
@@ -478,7 +479,7 @@ public class SeriesRecording implements Parcelable {
}
/**
- * The channelId to match.
+ * The channelId to match. The channel ID might not be valid when the channel option is "ALL".
*/
public long getChannelId() {
return mChannelId;
@@ -534,7 +535,6 @@ public class SeriesRecording implements Parcelable {
*
* <p>SeriesId is an opaque but stable string.
*/
- @NonNull
public String getSeriesId() {
return mSeriesId;
}
@@ -590,6 +590,13 @@ public class SeriesRecording implements Parcelable {
return mState;
}
+ /**
+ * Checks whether the series recording is stopped or not.
+ */
+ public boolean isStopped() {
+ return mState == STATE_SERIES_STOPPED;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -695,7 +702,7 @@ public class SeriesRecording implements Parcelable {
* episode constraints.
*/
public boolean matchProgram(Program program) {
- return matchProgram(program, true);
+ return matchProgram(program, mChannelOption);
}
/**
@@ -703,13 +710,12 @@ public class SeriesRecording implements Parcelable {
* episode constraints. It checks the channel option only if {@code checkChannelOption} is
* {@code true}.
*/
- public boolean matchProgram(Program program, boolean checkChannelOption) {
+ public boolean matchProgram(Program program, @ChannelOption int channelOption) {
String seriesId = program.getSeriesId();
long channelId = program.getChannelId();
String seasonNumber = program.getSeasonNumber();
String episodeNumber = program.getEpisodeNumber();
- if (!mSeriesId.equals(seriesId) || (checkChannelOption
- && mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE
+ if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE
&& mChannelId != channelId)) {
return false;
}
diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java
index 9e9b3add..5ed12ce8 100644
--- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/SeriesRecordingScheduler.java
@@ -20,19 +20,14 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.media.tv.TvContract;
-import android.media.tv.TvContract.Programs;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
-import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import android.util.LongSparseArray;
import com.android.tv.ApplicationSingletons;
import com.android.tv.TvApplication;
@@ -43,10 +38,8 @@ import com.android.tv.data.Program;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
+import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode;
import com.android.tv.experiments.Experiments;
-import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
-import com.android.tv.util.AsyncDbTask.CursorFilter;
-import com.android.tv.util.PermissionUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -59,7 +52,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArraySet;
import java.util.Set;
/**
@@ -70,20 +63,7 @@ import java.util.Set;
@TargetApi(Build.VERSION_CODES.N)
public class SeriesRecordingScheduler {
private static final String TAG = "SeriesRecordingSchd";
-
- private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID);
- private static final int RECORDING_PROHIBITED_INDEX =
- Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED);
-
- private static final String PARAM_START_TIME = "start_time";
- private static final String PARAM_END_TIME = "end_time";
-
- private static final String PROGRAM_SELECTION =
- Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND (" +
- Programs.COLUMN_SEASON_DISPLAY_NUMBER + " IS NOT NULL OR " +
- Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " IS NOT NULL) AND " +
- Programs.COLUMN_RECORDING_PROHIBITED + "=0";
- private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?";
+ private static final boolean DEBUG = false;
private static final String KEY_FETCHED_SERIES_IDS =
"SeriesRecordingScheduler.fetched_series_ids";
@@ -109,6 +89,11 @@ public class SeriesRecordingScheduler {
private final Set<String> mFetchedSeriesIds = new ArraySet<>();
private final SharedPreferences mSharedPreferences;
private boolean mStarted;
+ private boolean mPaused;
+ private final Set<Long> mPendingSeriesRecordings = new ArraySet<>();
+ private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners =
+ new CopyOnWriteArraySet<>();
+
private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() {
@Override
@@ -124,7 +109,7 @@ public class SeriesRecordingScheduler {
for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
iter.hasNext(); ) {
SeriesRecordingUpdateTask task = iter.next();
- if (CollectionUtils.subtract(task.mSeriesRecordings, seriesRecordings,
+ if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings,
SeriesRecording.ID_COMPARATOR).isEmpty()) {
task.cancel(true);
iter.remove();
@@ -134,7 +119,21 @@ public class SeriesRecordingScheduler {
@Override
public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
- updateSchedules(Arrays.asList(seriesRecordings));
+ List<SeriesRecording> stopped = new ArrayList<>();
+ List<SeriesRecording> normal = new ArrayList<>();
+ for (SeriesRecording r : seriesRecordings) {
+ if (r.isStopped()) {
+ stopped.add(r);
+ } else {
+ normal.add(r);
+ }
+ }
+ if (!stopped.isEmpty()) {
+ onSeriesRecordingRemoved(SeriesRecording.toArray(stopped));
+ }
+ if (!normal.isEmpty()) {
+ updateSchedules(normal);
+ }
}
};
@@ -174,8 +173,6 @@ public class SeriesRecordingScheduler {
Set<Long> seriesRecordingIds = new HashSet<>();
for (ScheduledRecording r : schedules) {
if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
- SoftPreconditions.checkState(r.getState()
- != ScheduledRecording.STATE_RECORDING_FINISHED);
seriesRecordingIds.add(r.getSeriesRecordingId());
}
}
@@ -214,6 +211,7 @@ public class SeriesRecordingScheduler {
if (mStarted) {
return;
}
+ if (DEBUG) Log.d(TAG, "start");
mStarted = true;
mDataManager.addSeriesRecordingListener(mSeriesRecordingListener);
mDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
@@ -226,13 +224,16 @@ public class SeriesRecordingScheduler {
if (!mStarted) {
return;
}
+ if (DEBUG) Log.d(TAG, "stop");
mStarted = false;
for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) {
task.cancel(true);
}
+ mFetchSeriesInfoTasks.clear();
for (SeriesRecordingUpdateTask task : mScheduleTasks) {
task.cancel(true);
}
+ mScheduleTasks.clear();
mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener);
}
@@ -254,26 +255,81 @@ public class SeriesRecordingScheduler {
}
/**
- * Creates/Updates the schedules for all the series recordings.
+ * Pauses the updates of the series recordings.
*/
- @MainThread
- public void updateSchedules() {
+ public void pauseUpdate() {
+ if (DEBUG) Log.d(TAG, "Schedule paused");
+ if (mPaused) {
+ return;
+ }
+ mPaused = true;
if (!mStarted) {
return;
}
- updateSchedules(mDataManager.getSeriesRecordings());
+ for (SeriesRecordingUpdateTask task : mScheduleTasks) {
+ for (SeriesRecording r : task.getSeriesRecordings()) {
+ mPendingSeriesRecordings.add(r.getId());
+ }
+ task.cancel(true);
+ }
+ }
+
+ /**
+ * Resumes the updates of the series recordings.
+ */
+ public void resumeUpdate() {
+ if (DEBUG) Log.d(TAG, "Schedule resumed");
+ if (!mPaused) {
+ return;
+ }
+ mPaused = false;
+ if (!mStarted) {
+ return;
+ }
+ if (!mPendingSeriesRecordings.isEmpty()) {
+ List<SeriesRecording> seriesRecordings = new ArrayList<>();
+ for (long seriesRecordingId : mPendingSeriesRecordings) {
+ SeriesRecording seriesRecording =
+ mDataManager.getSeriesRecording(seriesRecordingId);
+ if (seriesRecording != null) {
+ seriesRecordings.add(seriesRecording);
+ }
+ }
+ if (!seriesRecordings.isEmpty()) {
+ updateSchedules(seriesRecordings);
+ }
+ }
}
- private void updateSchedules(Collection<SeriesRecording> seriesRecordings) {
+ /**
+ * Update schedules for the given series recordings. If it's paused, the update will be done
+ * after it's resumed.
+ */
+ public void updateSchedules(Collection<SeriesRecording> seriesRecordings) {
+ if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings);
+ if (!mStarted) {
+ if (DEBUG) Log.d(TAG, "Not started yet.");
+ return;
+ }
+ if (mPaused) {
+ for (SeriesRecording r : seriesRecordings) {
+ mPendingSeriesRecordings.add(r.getId());
+ }
+ if (DEBUG) {
+ Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size="
+ + mPendingSeriesRecordings.size());
+ }
+ return;
+ }
Set<SeriesRecording> previousSeriesRecordings = new HashSet<>();
for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
iter.hasNext(); ) {
SeriesRecordingUpdateTask task = iter.next();
- if (CollectionUtils.containsAny(task.mSeriesRecordings, seriesRecordings,
+ if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings,
SeriesRecording.ID_COMPARATOR)) {
// The task is affected by the seriesRecordings
task.cancel(true);
- previousSeriesRecordings.addAll(task.mSeriesRecordings);
+ previousSeriesRecordings.addAll(task.getSeriesRecordings());
iter.remove();
}
}
@@ -281,38 +337,44 @@ public class SeriesRecordingScheduler {
previousSeriesRecordings, SeriesRecording.ID_COMPARATOR);
for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator();
iter.hasNext(); ) {
- if (mDataManager.getSeriesRecording(iter.next().getId()) == null) {
- // Series recording has been removed.
+ SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId());
+ if (seriesRecording == null || seriesRecording.isStopped()) {
+ // Series recording has been removed or stopped.
iter.remove();
}
}
if (seriesRecordingsToUpdate.isEmpty()) {
return;
}
- List<SeriesRecordingUpdateTask> tasksToRun = new ArrayList<>();
if (needToReadAllChannels(seriesRecordingsToUpdate)) {
- SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask(seriesRecordingsToUpdate,
- createSqlParams(seriesRecordingsToUpdate, null));
- tasksToRun.add(task);
+ SeriesRecordingUpdateTask task =
+ new SeriesRecordingUpdateTask(seriesRecordingsToUpdate);
mScheduleTasks.add(task);
+ if (DEBUG) Log.d(TAG, "Added schedule task: " + task);
+ task.execute();
} else {
for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask(
- Collections.singletonList(seriesRecording),
- createSqlParams(Collections.singletonList(seriesRecording), null));
- tasksToRun.add(task);
+ Collections.singletonList(seriesRecording));
mScheduleTasks.add(task);
+ if (DEBUG) Log.d(TAG, "Added schedule task: " + task);
+ task.execute();
}
}
- if (mDataManager.isDvrScheduleLoadFinished()) {
- runTasks(tasksToRun);
- }
}
- private void runTasks(List<SeriesRecordingUpdateTask> tasks) {
- for (SeriesRecordingUpdateTask task : tasks) {
- task.executeOnDbThread();
- }
+ /**
+ * Adds {@link OnSeriesRecordingUpdatedListener}.
+ */
+ public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
+ mOnSeriesRecordingUpdatedListeners.add(listener);
+ }
+
+ /**
+ * Removes {@link OnSeriesRecordingUpdatedListener}.
+ */
+ public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
+ mOnSeriesRecordingUpdatedListeners.remove(listener);
}
private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) {
@@ -325,94 +387,6 @@ public class SeriesRecordingScheduler {
}
/**
- * Queries the programs which are related to the series.
- * <p>
- * This is called from the UI when the series recording is created.
- */
- public void queryPrograms(SeriesRecording series, ProgramLoadCallback callback) {
- SoftPreconditions.checkState(mDataManager.isInitialized());
- Set<ScheduledEpisode> scheduledEpisodes = new HashSet<>();
- for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) {
- if (series.getSeriesId().equals(recordedProgram.getSeriesId())) {
- scheduledEpisodes.add(new ScheduledEpisode(series.getId(),
- recordedProgram.getSeasonNumber(), recordedProgram.getEpisodeNumber()));
- }
- }
- SqlParams sqlParams = createSqlParams(Collections.singletonList(series), scheduledEpisodes);
- new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection,
- sqlParams.selectionArgs, null, sqlParams.filter) {
- @Override
- protected void onPostExecute(List<Program> programs) {
- SoftPreconditions.checkNotNull(programs);
- if (programs == null) {
- Log.e(TAG, "Creating schedules for series recording failed: " + series);
- callback.onProgramLoadFinished(Collections.emptyList());
- } else {
- Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode(
- Collections.singletonList(series), programs);
- callback.onProgramLoadFinished(seriesProgramMap.get(series.getId()));
- }
- }
- }.executeOnDbThread();
- // To shorten the response time from UI, cancel and restart the background job.
- restartTasks();
- }
-
- private void restartTasks() {
- Set<SeriesRecording> seriesRecordings = new HashSet<>();
- for (SeriesRecordingUpdateTask task : mScheduleTasks) {
- seriesRecordings.addAll(task.mSeriesRecordings);
- task.cancel(true);
- }
- mScheduleTasks.clear();
- updateSchedules(seriesRecordings);
- }
-
- private SqlParams createSqlParams(List<SeriesRecording> seriesRecordings,
- Set<ScheduledEpisode> scheduledEpisodes) {
- SqlParams sqlParams = new SqlParams();
- if (PermissionUtils.hasAccessAllEpg(mContext)) {
- sqlParams.uri = Programs.CONTENT_URI;
- if (needToReadAllChannels(seriesRecordings)) {
- sqlParams.selection = PROGRAM_SELECTION;
- sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis())};
- } else {
- SoftPreconditions.checkArgument(seriesRecordings.size() == 1);
- sqlParams.selection = PROGRAM_SELECTION + " AND " + CHANNEL_ID_PREDICATE;
- sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis()),
- Long.toString(seriesRecordings.get(0).getChannelId())};
- }
- sqlParams.filter = new SeriesRecordingCursorFilter(seriesRecordings, scheduledEpisodes);
- } else {
- if (needToReadAllChannels(seriesRecordings)) {
- sqlParams.uri = Programs.CONTENT_URI.buildUpon()
- .appendQueryParameter(PARAM_START_TIME,
- String.valueOf(System.currentTimeMillis()))
- .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE))
- .build();
- } else {
- SoftPreconditions.checkArgument(seriesRecordings.size() == 1);
- sqlParams.uri = TvContract.buildProgramsUriForChannel(
- seriesRecordings.get(0).getChannelId(),
- System.currentTimeMillis(), Long.MAX_VALUE);
- }
- sqlParams.selection = null;
- sqlParams.selectionArgs = null;
- sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(seriesRecordings,
- scheduledEpisodes);
- }
- return sqlParams;
- }
-
- @VisibleForTesting
- static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes,
- ScheduledEpisode episode) {
- // The episode whose season number or episode number is null will always be scheduled.
- return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber)
- && !TextUtils.isEmpty(episode.episodeNumber);
- }
-
- /**
* Pick one program per an episode.
*
* <p>Note that the programs which has been already scheduled have the highest priority, and all
@@ -421,7 +395,7 @@ public class SeriesRecordingScheduler {
* <p>If there are no existing schedules for an episode, one program which starts earlier is
* picked.
*/
- private Map<Long, List<Program>> pickOneProgramPerEpisode(
+ private LongSparseArray<List<Program>> pickOneProgramPerEpisode(
List<SeriesRecording> seriesRecordings, List<Program> programs) {
return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs);
}
@@ -430,10 +404,11 @@ public class SeriesRecordingScheduler {
* @see #pickOneProgramPerEpisode(List, List)
*/
@VisibleForTesting
- static Map<Long, List<Program>> pickOneProgramPerEpisode(DvrDataManager dataManager,
- List<SeriesRecording> seriesRecordings, List<Program> programs) {
+ static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
+ DvrDataManager dataManager, List<SeriesRecording> seriesRecordings,
+ List<Program> programs) {
// Initialize.
- Map<Long, List<Program>> result = new HashMap<>();
+ LongSparseArray<List<Program>> result = new LongSparseArray<>();
Map<String, Long> seriesRecordingIds = new HashMap<>();
for (SeriesRecording seriesRecording : seriesRecordings) {
result.put(seriesRecording.getId(), new ArrayList<>());
@@ -508,27 +483,27 @@ public class SeriesRecordingScheduler {
* This works only for the existing series recordings. Do not use this task for the
* "adding series recording" UI.
*/
- private class SeriesRecordingUpdateTask extends AsyncProgramQueryTask {
- private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>();
-
- SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings, SqlParams sqlParams) {
- super(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection,
- sqlParams.selectionArgs, null, sqlParams.filter);
- mSeriesRecordings.addAll(seriesRecordings);
+ private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask {
+ SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) {
+ super(mContext, seriesRecordings);
}
@Override
protected void onPostExecute(List<Program> programs) {
+ if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs);
mScheduleTasks.remove(this);
if (programs == null) {
- Log.e(TAG, "Creating schedules for series recording failed: " + mSeriesRecordings);
+ Log.e(TAG, "Creating schedules for series recording failed: "
+ + getSeriesRecordings());
return;
}
- Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode(
- mSeriesRecordings, programs);
- for (SeriesRecording seriesRecording : mSeriesRecordings) {
+ LongSparseArray<List<Program>> seriesProgramMap = pickOneProgramPerEpisode(
+ getSeriesRecordings(), programs);
+ for (SeriesRecording seriesRecording : getSeriesRecordings()) {
// Check the series recording is still valid.
- if (mDataManager.getSeriesRecording(seriesRecording.getId()) == null) {
+ SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording(
+ seriesRecording.getId());
+ if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) {
continue;
}
List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId());
@@ -537,122 +512,25 @@ public class SeriesRecordingScheduler {
mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
}
}
- }
-
- @Override
- protected void onCancelled(List<Program> programs) {
- mScheduleTasks.remove(this);
- }
- }
-
- /**
- * Filter the programs which match the series recording. The episodes which the schedules are
- * already created for are filtered out too.
- */
- private class SeriesRecordingCursorFilter implements CursorFilter {
- private final List<SeriesRecording> mSeriesRecording = new ArrayList<>();
- private final Set<Long> mDisallowedProgramIds = new HashSet<>();
- private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>();
-
- SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings,
- Set<ScheduledEpisode> scheduledEpisodes) {
- mSeriesRecording.addAll(seriesRecordings);
- mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds());
- Set<Long> seriesRecordingIds = new HashSet<>();
- for (SeriesRecording r : seriesRecordings) {
- seriesRecordingIds.add(r.getId());
- }
- if (scheduledEpisodes != null) {
- mScheduledEpisodes.addAll(scheduledEpisodes);
- }
- for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
- if (seriesRecordingIds.contains(r.getSeriesRecordingId())
- && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
- && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
- mScheduledEpisodes.add(new ScheduledEpisode(r));
+ if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) {
+ for (OnSeriesRecordingUpdatedListener listener
+ : mOnSeriesRecordingUpdatedListeners) {
+ listener.onSeriesRecordingUpdated(
+ SeriesRecording.toArray(getSeriesRecordings()));
}
}
}
@Override
- @WorkerThread
- public boolean filter(Cursor c) {
- if (mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
- return false;
- }
- Program program = Program.fromCursor(c);
- for (SeriesRecording seriesRecording : mSeriesRecording) {
- boolean programMatches = seriesRecording.matchProgram(program);
- if (programMatches && !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode(
- seriesRecording.getId(), program.getSeasonNumber(),
- program.getEpisodeNumber()))) {
- return true;
- }
- }
- return false;
- }
- }
-
- private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter {
- SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings,
- Set<ScheduledEpisode> scheduledEpisodes) {
- super(seriesRecordings, scheduledEpisodes);
- }
-
- @Override
- public boolean filter(Cursor c) {
- return c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c);
- }
- }
-
- private static class SqlParams {
- public Uri uri;
- public String selection;
- public String[] selectionArgs;
- public CursorFilter filter;
- }
-
- @VisibleForTesting
- static class ScheduledEpisode {
- public final long seriesRecordingId;
- public final String seasonNumber;
- public final String episodeNumber;
-
- /**
- * Create a new Builder with the values set from an existing {@link ScheduledRecording}.
- */
- ScheduledEpisode(ScheduledRecording r) {
- this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
- }
-
- public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) {
- this.seriesRecordingId = seriesRecordingId;
- this.seasonNumber = seasonNumber;
- this.episodeNumber = episodeNumber;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof ScheduledEpisode)) return false;
- ScheduledEpisode that = (ScheduledEpisode) o;
- return seriesRecordingId == that.seriesRecordingId
- && Objects.equals(seasonNumber, that.seasonNumber)
- && Objects.equals(episodeNumber, that.episodeNumber);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
+ protected void onCancelled(List<Program> programs) {
+ mScheduleTasks.remove(this);
}
@Override
public String toString() {
- return "ScheduledEpisode{" +
- "seriesRecordingId=" + seriesRecordingId +
- ", seasonNumber='" + seasonNumber +
- ", episodeNumber=" + episodeNumber +
- '}';
+ return "SeriesRecordingUpdateTask:{"
+ + "series_recordings=" + getSeriesRecordings()
+ + "}";
}
}
@@ -663,10 +541,6 @@ public class SeriesRecordingScheduler {
mSeriesRecording = seriesRecording;
}
- String getSeriesId() {
- return mSeriesRecording.getSeriesId();
- }
-
@Override
protected SeriesInfo doInBackground(Void... voids) {
return EpgFetcher.createEpgReader(mContext)
@@ -697,9 +571,9 @@ public class SeriesRecordingScheduler {
}
/**
- * Called when the program loading is finished for the series recording.
+ * A listener to notify when series recording are updated.
*/
- public interface ProgramLoadCallback {
- void onProgramLoadFinished(@NonNull List<Program> programs);
+ public interface OnSeriesRecordingUpdatedListener {
+ void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings);
}
}
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index 382f7112..bf72d912 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -44,9 +44,13 @@ interface WritableDvrDataManager extends DvrDataManager {
void removeScheduledRecording(ScheduledRecording... scheduledRecordings);
/**
+ * Removes recordings. If {@code forceRemove} is {@code true}, the schedule will be permanently
+ * removed instead of changing the state to DELETED.
+ */
+ void removeScheduledRecording(boolean forceRemove, ScheduledRecording... scheduledRecordings);
+
+ /**
* Removes series recordings.
- *
- * <p>Note that the finished or failed schedules are not deleted.
*/
void removeSeriesRecording(SeriesRecording... seasonSchedules);
@@ -64,4 +68,11 @@ interface WritableDvrDataManager extends DvrDataManager {
* Changes the state of the recording.
*/
void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState);
+
+ /**
+ * Remove all the records related to the input.
+ * <p>
+ * Note that this should be called after the input was removed.
+ */
+ void forgetStorage(String inputId);
}
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index 3fe2d211..f0aca18e 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -233,9 +233,9 @@ public final class DvrContract {
public static final String STATE_SERIES_NORMAL = "STATE_SERIES_NORMAL";
/**
- * The state indicates that it is a canceled one.
+ * The state indicates that it is stopped.
*/
- public static final String STATE_SERIES_CANCELED = "STATE_SERIES_CANCELED";
+ public static final String STATE_SERIES_STOPPED = "STATE_SERIES_STOPPED";
/**
* The priority of this recording.
@@ -380,7 +380,7 @@ public final class DvrContract {
* The state of whether the series recording be canceled or not.
*
* <p>This value should be one of the followings: {@link #STATE_SERIES_NORMAL} and
- * {@link #STATE_SERIES_CANCELED}. The default value is STATE_SERIES_NORMAL.
+ * {@link #STATE_SERIES_STOPPED}. The default value is STATE_SERIES_NORMAL.
*
* <p>Type: TEXT
*/
diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
index d6e17161..175f05bc 100644
--- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
@@ -16,27 +16,285 @@
package com.android.tv.dvr.ui;
-import android.content.Context;
-import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
+import android.app.Activity;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.support.v17.leanback.widget.Presenter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.tv.R;
+import com.android.tv.ui.ViewUtils;
import com.android.tv.util.Utils;
/**
- * Presents a {@link DetailsContent}.
+ * An {@link Presenter} for rendering a detailed description of an DVR item.
+ * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}.
+ * Most codes of this class is originated from
+ * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
+ * The latter class are re-used to provide a customized version of
+ * {@link android.support.v17.leanback.widget.DetailsOverviewRow}.
*/
-public class DetailsContentPresenter extends AbstractDetailsDescriptionPresenter {
+public class DetailsContentPresenter extends Presenter {
+ /**
+ * The ViewHolder for the {@link DetailsContentPresenter}.
+ */
+ public static class ViewHolder extends Presenter.ViewHolder {
+ final TextView mTitle;
+ final TextView mSubtitle;
+ final LinearLayout mDescriptionContainer;
+ final TextView mBody;
+ final TextView mReadMoreView;
+ final int mTitleMargin;
+ final int mUnderTitleBaselineMargin;
+ final int mUnderSubtitleBaselineMargin;
+ final int mTitleLineSpacing;
+ final int mBodyLineSpacing;
+ final int mBodyMaxLines;
+ final int mBodyMinLines;
+ final FontMetricsInt mTitleFontMetricsInt;
+ final FontMetricsInt mSubtitleFontMetricsInt;
+ final FontMetricsInt mBodyFontMetricsInt;
+ final int mTitleMaxLines;
+
+ private Activity mActivity;
+ private boolean mFullTextMode;
+ private int mFullTextAnimationDuration;
+ private boolean mIsListeningToPreDraw;
+
+ private ViewTreeObserver.OnPreDrawListener mPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (mSubtitle.getVisibility() == View.VISIBLE
+ && mSubtitle.getTop() > view.getHeight()
+ && mTitle.getLineCount() > 1) {
+ mTitle.setMaxLines(mTitle.getLineCount() - 1);
+ return false;
+ }
+ final int bodyLines = mBody.getLineCount();
+ final int maxLines = mFullTextMode ? bodyLines :
+ (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines);
+ if (bodyLines > maxLines) {
+ mReadMoreView.setVisibility(View.VISIBLE);
+ mDescriptionContainer.setFocusable(true);
+ mDescriptionContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mFullTextMode = true;
+ mReadMoreView.setVisibility(View.GONE);
+ mDescriptionContainer.setFocusable(false);
+ mDescriptionContainer.setOnClickListener(null);
+ mBody.setMaxLines(bodyLines);
+ // Minus 1 from line difference to eliminate the space
+ // originally occupied by "READ MORE"
+ showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing);
+ }
+ });
+ }
+ if (mBody.getMaxLines() != maxLines) {
+ mBody.setMaxLines(maxLines);
+ return false;
+ } else {
+ removePreDrawListener();
+ return true;
+ }
+ }
+ };
+
+ public ViewHolder(final View view) {
+ super(view);
+ mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
+ mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
+ mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
+ mDescriptionContainer =
+ (LinearLayout) view.findViewById(R.id.dvr_details_description_container);
+ mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more);
+
+ FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle);
+ final int titleAscent = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_baseline);
+ // Ascent is negative
+ mTitleMargin = titleAscent + titleFontMetricsInt.ascent;
+
+ mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_title_baseline_margin);
+ mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_subtitle_baseline_margin);
+
+ mTitleLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_line_spacing);
+ mBodyLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_body_line_spacing);
+
+ mBodyMaxLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_max_lines);
+ mBodyMinLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_min_lines);
+ mTitleMaxLines = mTitle.getMaxLines();
+
+ mTitleFontMetricsInt = getFontMetricsInt(mTitle);
+ mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle);
+ mBodyFontMetricsInt = getFontMetricsInt(mBody);
+ }
+
+ void addPreDrawListener() {
+ if (!mIsListeningToPreDraw) {
+ mIsListeningToPreDraw = true;
+ view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
+ }
+ }
+
+ void removePreDrawListener() {
+ if (mIsListeningToPreDraw) {
+ view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
+ mIsListeningToPreDraw = false;
+ }
+ }
+
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ public TextView getSubtitle() {
+ return mSubtitle;
+ }
+
+ public TextView getBody() {
+ return mBody;
+ }
+
+ private FontMetricsInt getFontMetricsInt(TextView textView) {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTextSize(textView.getTextSize());
+ paint.setTypeface(textView.getTypeface());
+ return paint.getFontMetricsInt();
+ }
+
+ private void showFullText(int heightDiff) {
+ final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame);
+ int nowHeight = ViewUtils.getLayoutHeight(detailsFrame);
+ Animator expandAnimator = ViewUtils.createHeightAnimator(
+ detailsFrame, nowHeight, nowHeight + heightDiff);
+ expandAnimator.setDuration(mFullTextAnimationDuration);
+ Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+ 0f, -(heightDiff / 2)));
+ shiftAnimator.setDuration(mFullTextAnimationDuration);
+ AnimatorSet fullTextAnimator = new AnimatorSet();
+ fullTextAnimator.playTogether(expandAnimator, shiftAnimator);
+ fullTextAnimator.start();
+ }
+ }
+
+ private final Activity mActivity;
+ private final int mFullTextAnimationDuration;
+
+ public DetailsContentPresenter(Activity activity) {
+ super();
+ mActivity = activity;
+ mFullTextAnimationDuration = mActivity.getResources()
+ .getInteger(R.integer.dvr_details_full_text_animation_duration);
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.dvr_details_description, parent, false);
+ return new ViewHolder(v);
+ }
+
@Override
- protected void onBindDescription(final ViewHolder viewHolder, Object itemData) {
- DetailsContent detailsContent = (DetailsContent) itemData;
- Context context = viewHolder.view.getContext();
- viewHolder.getTitle().setText(detailsContent.getTitle());
+ public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ final ViewHolder vh = (ViewHolder) viewHolder;
+ final DetailsContent detailsContent = (DetailsContent) item;
+
+ vh.mActivity = mActivity;
+ vh.mFullTextAnimationDuration = mFullTextAnimationDuration;
+
+ boolean hasTitle = true;
+ if (TextUtils.isEmpty(detailsContent.getTitle())) {
+ vh.mTitle.setVisibility(View.GONE);
+ hasTitle = false;
+ } else {
+ vh.mTitle.setText(detailsContent.getTitle());
+ vh.mTitle.setVisibility(View.VISIBLE);
+ vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight()
+ + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier());
+ vh.mTitle.setMaxLines(vh.mTitleMaxLines);
+ }
+ setTopMargin(vh.mTitle, vh.mTitleMargin);
+
+ boolean hasSubtitle = true;
if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME
&& detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) {
- String playTime = Utils.getDurationString(context,
+ vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(),
detailsContent.getStartTimeUtcMillis(),
- detailsContent.getEndTimeUtcMillis(), false);
- viewHolder.getSubtitle().setText(playTime);
+ detailsContent.getEndTimeUtcMillis(), false));
+ vh.mSubtitle.setVisibility(View.VISIBLE);
+ if (hasTitle) {
+ setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin
+ + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent);
+ } else {
+ setTopMargin(vh.mSubtitle, 0);
+ }
+ } else {
+ vh.mSubtitle.setVisibility(View.GONE);
+ hasSubtitle = false;
}
- viewHolder.getBody().setText(detailsContent.getDescription());
+
+ if (TextUtils.isEmpty(detailsContent.getDescription())) {
+ vh.mBody.setVisibility(View.GONE);
+ } else {
+ vh.mBody.setText(detailsContent.getDescription());
+ vh.mBody.setVisibility(View.VISIBLE);
+ vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight()
+ + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier());
+ if (hasSubtitle) {
+ setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin
+ + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent
+ - vh.mBody.getPaddingTop());
+ } else if (hasTitle) {
+ setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin
+ + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent
+ - vh.mBody.getPaddingTop());
+ } else {
+ setTopMargin(vh.mDescriptionContainer, 0);
+ }
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { }
+
+ @Override
+ public void onViewAttachedToWindow(Presenter.ViewHolder holder) {
+ // In case predraw listener was removed in detach, make sure
+ // we have the proper layout.
+ ViewHolder vh = (ViewHolder) holder;
+ vh.addPreDrawListener();
+ super.onViewAttachedToWindow(holder);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
+ ViewHolder vh = (ViewHolder) holder;
+ vh.removePreDrawListener();
+ super.onViewDetachedFromWindow(holder);
+ }
+
+ private void setTopMargin(View view, int topMargin) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
+ lp.topMargin = topMargin;
+ view.setLayoutParams(lp);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
index 37f152f9..6714ecd3 100644
--- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
@@ -76,13 +76,17 @@ public class DetailsViewBackgroundHelper {
* Sets the background color.
*/
public void setBackgroundColor(int color) {
- mBackgroundManager.setColor(color);
+ if (mBackgroundManager.isAttached()) {
+ mBackgroundManager.setColor(color);
+ }
}
/**
* Sets the background scrim.
*/
public void setScrim(int color) {
- mBackgroundManager.setDimLayer(new ColorDrawable(color));
+ if (mBackgroundManager.isAttached()) {
+ mBackgroundManager.setDimLayer(new ColorDrawable(color));
+ }
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index d7c2de88..9df228d1 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -96,7 +96,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
if (action.getId() == ACTION_RECORD_ANYWAY) {
getDvrManager().addSchedule(mProgram);
} else if (action.getId() == ACTION_WATCH) {
- DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null);
+ DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null, false);
}
dismissDialog();
}
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
index 74d0ba0b..a6dd31d1 100644
--- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
@@ -26,23 +26,25 @@ import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.TitleViewAdapter;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.GenreItems;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
-import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
-import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.RecordedProgram;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.Arrays;
@@ -64,7 +66,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
private RecordedProgramAdapter mRecentAdapter;
private ScheduleAdapter mScheduleAdapter;
- private RecordedProgramAdapter mSeriesAdapter;
+ private SeriesAdapter mSeriesAdapter;
private RecordedProgramAdapter[] mGenreAdapters =
new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
private ListRow mRecentRow;
@@ -72,7 +74,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
private List<String> mGenreLabels;
private DvrDataManager mDvrDataManager;
- private TvInputManagerHelper mTvInputManagerHelper;
+ private DvrScheduleManager mDvrScheudleManager;
private ArrayObjectAdapter mRowsAdapter;
private ClassPresenterSelector mPresenterSelector;
private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
@@ -107,7 +109,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
public int compare(Object lhs, Object rhs) {
if (lhs instanceof ScheduledRecording) {
if (rhs instanceof ScheduledRecording) {
- return ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
.compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
} else {
return -1;
@@ -120,36 +122,15 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
};
- private final TvInputCallback mTvInputCallback = new TvInputCallback() {
+ private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener =
+ new DvrScheduleManager.OnConflictStateChangeListener() {
@Override
- public void onInputAdded(String inputId) {
- List<ScheduledRecording> scheduleRecordings =
- mDvrDataManager.getScheduledRecordings(inputId);
- if (!scheduleRecordings.isEmpty()) {
- onScheduledRecordingStatusChanged(ScheduledRecording.toArray(scheduleRecordings));
- }
- handleSeriesRecordingsChanged(mDvrDataManager.getSeriesRecordings(inputId));
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- if (TextUtils.equals(recordedProgram.getInputId(), inputId)) {
- handleRecordedProgramChanged(recordedProgram);
+ public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) {
+ if (mScheduleAdapter != null) {
+ for (ScheduledRecording schedule : schedules) {
+ onScheduledRecordingStatusChanged(schedule);
}
}
- postUpdateRows();
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- List<ScheduledRecording> scheduleRecordings =
- mDvrDataManager.getScheduledRecordings(inputId);
- onScheduledRecordingRemoved(
- scheduleRecordings.toArray(new ScheduledRecording[scheduleRecordings.size()]));
- handleSeriesRecordingsRemoved(mDvrDataManager.getSeriesRecordings(inputId));
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- if (TextUtils.equals(recordedProgram.getInputId(), inputId)) {
- handleRecordedProgramRemoved(recordedProgram);
- }
- }
- postUpdateRows();
}
};
@@ -165,8 +146,9 @@ public class DvrBrowseFragment extends BrowseFragment implements
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
Context context = getContext();
- mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
- mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper();
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrScheudleManager = singletons.getDvrScheduleManager();
mPresenterSelector = new ClassPresenterSelector()
.addClassPresenter(ScheduledRecording.class,
new ScheduledRecordingPresenter(context))
@@ -177,7 +159,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
mGenreLabels.add(getString(R.string.dvr_main_others));
setupUiElements();
setupAdapters();
- mTvInputManagerHelper.addCallback(mTvInputCallback);
+ mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
prepareEntranceTransition();
if (mDvrDataManager.isInitialized()) {
startEntranceTransition();
@@ -194,9 +176,8 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
- super.onDestroy();
mHandler.removeCallbacks(mUpdateRowsRunnable);
- mTvInputManagerHelper.removeCallback(mTvInputCallback);
+ mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener);
mDvrDataManager.removeRecordedProgramListener(this);
mDvrDataManager.removeScheduledRecordingListener(this);
mDvrDataManager.removeSeriesRecordingListener(this);
@@ -204,6 +185,12 @@ public class DvrBrowseFragment extends BrowseFragment implements
mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
mRowsAdapter.clear();
mSeriesId2LatestProgram.clear();
+ for (Presenter presenter : mPresenterSelector.getPresenters()) {
+ if (presenter instanceof DvrItemPresenter) {
+ ((DvrItemPresenter) presenter).unbindAllViewHolders();
+ }
+ }
+ super.onDestroy();
}
@Override
@@ -221,9 +208,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public void onRecordedProgramLoadFinished() {
for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- if (isInputExist(recordedProgram.getInputId())) {
- handleRecordedProgramAdded(recordedProgram, true);
- }
+ handleRecordedProgramAdded(recordedProgram, true);
}
updateRows();
if (mDvrDataManager.isInitialized()) {
@@ -233,27 +218,27 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
@Override
- public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
- if (isInputExist(recordedProgram.getInputId())) {
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
handleRecordedProgramAdded(recordedProgram, true);
- postUpdateRows();
}
+ postUpdateRows();
}
@Override
- public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
- if (isInputExist(recordedProgram.getInputId())) {
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
handleRecordedProgramChanged(recordedProgram);
- postUpdateRows();
}
+ postUpdateRows();
}
@Override
- public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
- if (isInputExist(recordedProgram.getInputId())) {
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
handleRecordedProgramRemoved(recordedProgram);
- postUpdateRows();
}
+ postUpdateRows();
}
// No need to call updateRows() during ScheduledRecordings' change because
@@ -320,7 +305,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
private void setupAdapters() {
mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
- mSeriesAdapter = new RecordedProgramAdapter();
+ mSeriesAdapter = new SeriesAdapter();
for (int i = 0; i < mGenreAdapters.length; i++) {
mGenreAdapters[i] = new RecordedProgramAdapter();
}
@@ -330,9 +315,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
// Recorded Programs.
for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- if (isInputExist(recordedProgram.getInputId())) {
- handleRecordedProgramAdded(recordedProgram, false);
- }
+ handleRecordedProgramAdded(recordedProgram, false);
}
// Series Recordings. Series recordings should be added after recorded programs, because
// we build series recordings' latest program information while adding recorded programs.
@@ -426,13 +409,11 @@ public class DvrBrowseFragment extends BrowseFragment implements
private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) {
for (SeriesRecording seriesRecording : seriesRecordings) {
- if (isInputExist(seriesRecording.getInputId())) {
- mSeriesAdapter.add(seriesRecording);
- if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
- for (RecordedProgramAdapter adapter
- : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
- adapter.add(seriesRecording);
- }
+ mSeriesAdapter.add(seriesRecording);
+ if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
+ adapter.add(seriesRecording);
}
}
}
@@ -450,15 +431,13 @@ public class DvrBrowseFragment extends BrowseFragment implements
private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) {
for (SeriesRecording seriesRecording : seriesRecordings) {
- if (isInputExist(seriesRecording.getInputId())) {
- mSeriesAdapter.change(seriesRecording);
- if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
- updateGenreAdapters(getGenreAdapters(
- seriesRecording.getCanonicalGenreIds()), seriesRecording);
- } else {
- // Remove series recording from all genre rows if it has no recorded program
- updateGenreAdapters(new ArrayList<>(), seriesRecording);
- }
+ mSeriesAdapter.change(seriesRecording);
+ if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
+ updateGenreAdapters(getGenreAdapters(
+ seriesRecording.getCanonicalGenreIds()), seriesRecording);
+ } else {
+ // Remove series recording from all genre rows if it has no recorded program
+ updateGenreAdapters(new ArrayList<>(), seriesRecording);
}
}
}
@@ -545,23 +524,18 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
}
- private boolean isInputExist(String inputId) {
- return mTvInputManagerHelper.getTvInputInfo(inputId) != null;
- }
-
private boolean needToShowScheduledRecording(ScheduledRecording recording) {
int state = recording.getState();
- return isInputExist(recording.getInputId())
- && (state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
- || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED);
+ return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
}
private void updateLatestRecordedProgram(SeriesRecording seriesRecording) {
RecordedProgram latestProgram = null;
for (RecordedProgram program :
mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) {
- if (isInputExist(program.getInputId()) && (latestProgram == null || RecordedProgram
- .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0)) {
+ if (latestProgram == null || RecordedProgram
+ .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) {
latestProgram = program;
}
}
@@ -583,6 +557,27 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
}
+ private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> {
+ SeriesAdapter() {
+ super(mPresenterSelector, new Comparator<SeriesRecording>() {
+ @Override
+ public int compare(SeriesRecording lhs, SeriesRecording rhs) {
+ if (lhs.isStopped() && !rhs.isStopped()) {
+ return 1;
+ } else if (!lhs.isStopped() && rhs.isStopped()) {
+ return -1;
+ }
+ return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
+ }
+ });
+ }
+
+ @Override
+ public long getId(SeriesRecording item) {
+ return item.getId();
+ }
+ }
+
private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
RecordedProgramAdapter() {
this(Integer.MAX_VALUE);
diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java
deleted file mode 100644
index 78f73fd5..00000000
--- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.app.DialogFragment;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.v17.leanback.widget.GuidanceStylist;
-import android.support.v17.leanback.widget.GuidedAction;
-
-import com.android.tv.R;
-
-import java.util.List;
-
-/**
- * A fragment which asks the user to cancel all series schedules recordings.
- */
-public class DvrCancelAllSeriesRecordingFragment extends DvrGuidedStepFragment {
- private static final int ACTION_CANCEL_ALL = 1;
- private static final int ACTION_BACK = 2;
-
- @Override
- public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.dvr_series_schedules_dialog_cancel_all);
- Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete);
- return new GuidanceStylist.Guidance(title, null, null, icon);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_CANCEL_ALL)
- .title(getResources().getString(R.string.dvr_series_schedules_cancel_all))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_BACK)
- .title(getResources().getString(R.string.dvr_series_schedules_dialog_back))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- DvrSchedulesActivity activity = (DvrSchedulesActivity) getActivity();
- if (action.getId() == ACTION_CANCEL_ALL) {
- activity.onCancelAllClicked();
- }
- dismissDialog();
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
index fe65eebd..837d8ab2 100644
--- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
@@ -26,7 +26,6 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
-import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment;
@@ -38,7 +37,6 @@ import java.util.concurrent.TimeUnit;
public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragment {
private final List<Long> mDurations = new ArrayList<>();
private Channel mChannel;
- private Program mProgram;
@Override
public void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
index b273c85c..806c775c 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
@@ -21,6 +21,7 @@ import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
import com.android.tv.R;
+import com.android.tv.TvApplication;
/**
* Activity to show details view in DVR.
@@ -69,6 +70,7 @@ public class DvrDetailsActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_details);
long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
index be995fcb..21f9c4b4 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
@@ -17,10 +17,14 @@
package com.android.tv.dvr.ui;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v17.leanback.app.DetailsFragment;
@@ -36,11 +40,22 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
+import android.widget.Toast;
import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrPlaybackActivity;
+import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.ImageLoader;
+import com.android.tv.util.ToastUtils;
+import com.android.tv.util.Utils;
+
+import java.io.File;
abstract class DvrDetailsFragment extends DetailsFragment {
private static final int LOAD_LOGO_IMAGE = 1;
@@ -59,6 +74,7 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
setupAdapter();
+ onCreateInternal();
}
@Override
@@ -74,8 +90,8 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
private void setupAdapter() {
- DetailsOverviewRowPresenter rowPresenter =
- new DetailsOverviewRowPresenter(new DetailsContentPresenter());
+ DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter(
+ new DetailsContentPresenter(getActivity()));
rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background,
null));
rowPresenter.setSharedElementEnterTransition(getActivity(),
@@ -105,13 +121,22 @@ abstract class DvrDetailsFragment extends DetailsFragment {
/**
* Creates and returns presenter selector will be used by rows adaptor.
*/
- protected PresenterSelector onCreatePresenterSelector(DetailsOverviewRowPresenter rowPresenter) {
+ protected PresenterSelector onCreatePresenterSelector(
+ DetailsOverviewRowPresenter rowPresenter) {
ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
return presenterSelector;
}
/**
+ * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish
+ * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not
+ * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to
+ * do after the super class did onCreate, it should override this method and put the codes here.
+ */
+ protected void onCreateInternal() { }
+
+ /**
* Updates actions of details overview.
*/
protected void updateActions() {
@@ -198,6 +223,84 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
}
+ protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) {
+ if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) &&
+ !isDataUriAccessible(recordedProgram.getDataUri())) {
+ // Since cleaning RecordedProgram from forgotten storage will take some time,
+ // ignore playback until cleaning is finished.
+ ToastUtils.show(getContext(),
+ getContext().getResources().getString(R.string.dvr_toast_recording_deleted),
+ Toast.LENGTH_SHORT);
+ return;
+ }
+ ParentalControlSettings parental = TvApplication.getSingletons(getActivity())
+ .getTvInputManagerHelper().getParentalControlSettings();
+ if (!parental.isParentalControlsEnabled()) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ return;
+ }
+ ChannelDataManager channelDataManager =
+ TvApplication.getSingletons(getActivity()).getChannelDataManager();
+ Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId());
+ if (channel != null && channel.isLocked()) {
+ checkPinToPlay(recordedProgram, seekTimeMs);
+ return;
+ }
+ String ratingString = recordedProgram.getContentRating();
+ if (TextUtils.isEmpty(ratingString)) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ return;
+ }
+ String[] ratingList = ratingString.split(",");
+ TvContentRating[] programRatings = new TvContentRating[ratingList.length];
+ for (int i = 0; i < ratingList.length; i++) {
+ programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]);
+ }
+ TvContentRating blockRatings = parental.getBlockedRating(programRatings);
+ if (blockRatings != null) {
+ checkPinToPlay(recordedProgram, seekTimeMs);
+ } else {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ }
+ }
+
+ private boolean isDataUriAccessible(Uri dataUri) {
+ if (dataUri == null || dataUri.getPath() == null) {
+ return false;
+ }
+ try {
+ File recordedProgramPath = new File(dataUri.getPath());
+ if (recordedProgramPath.exists()) {
+ return true;
+ }
+ } catch (SecurityException e) {
+ }
+ return false;
+ }
+
+ private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) {
+ new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+ new PinDialogFragment.ResultListener() {
+ @Override
+ public void done(boolean success) {
+ if (success) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, true);
+ }
+ }
+ }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
+ }
+
+ private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs,
+ boolean pinChecked) {
+ Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId());
+ if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs);
+ }
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked);
+ getActivity().startActivity(intent);
+ }
+
private static class MyImageLoaderCallback extends
ImageLoader.ImageLoaderCallback<DvrDetailsFragment> {
private final Context mContext;
diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
index 6f287c70..73ddcdd0 100644
--- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
@@ -28,8 +28,6 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
import java.util.List;
@@ -77,14 +75,7 @@ public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment {
return;
}
DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
- DvrDataManager dataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
- List<SeriesRecording> seriesRecordings = dataManager.getSeriesRecordings(mInputId);
- for(SeriesRecording series : seriesRecordings) {
- dvrManager.removeSeriesRecording(series.getId());
- }
- List<ScheduledRecording> scheduledRecordings = dataManager.getScheduledRecordings(mInputId);
- dvrManager.removeScheduledRecording(ScheduledRecording.toArray(scheduledRecordings));
- dvrManager.removeRecordedProgramByMissingStorage(mInputId);
+ dvrManager.forgetStorage(mInputId);
Activity activity = getActivity();
if (activity instanceof DvrDetailsActivity) {
// Since we removed everything, just finish the activity.
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index eaccd8ed..d26e6836 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -20,6 +20,7 @@ import android.app.DialogFragment;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.VerticalGridView;
import android.view.LayoutInflater;
import android.view.View;
@@ -30,9 +31,11 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener;
public class DvrGuidedStepFragment extends GuidedStepFragment {
private DvrManager mDvrManager;
+ private OnActionClickListener mOnActionClickListener;
protected DvrManager getDvrManager() {
return mDvrManager;
@@ -60,6 +63,14 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
return R.style.Theme_TV_Dvr_GuidedStep;
}
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (mOnActionClickListener != null) {
+ mOnActionClickListener.onActionClick(action.getId());
+ }
+ dismissDialog();
+ }
+
protected void dismissDialog() {
if (getActivity() instanceof MainActivity) {
SafeDismissDialogFragment currentDialog =
@@ -71,4 +82,8 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
((DialogFragment) getParentFragment()).dismiss();
}
}
-}
+
+ protected void setOnActionClickListener(OnActionClickListener listener) {
+ mOnActionClickListener = listener;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index 50187a56..2b132db8 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -20,17 +20,21 @@ import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.data.ParcelableList;
+import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.guide.ProgramGuide;
+import java.util.List;
+
public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/**
* Key for input ID.
@@ -43,11 +47,6 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
*/
public static final String KEY_PROGRAM = "DvrHalfSizedDialogFragment.program";
/**
- * Key for the programs.
- * Type: {@link ParcelableList}.
- */
- public static final String KEY_PROGRAMS = "DvrHalfSizedDialogFragment.programs";
- /**
* Key for the channel ID.
* Type: long.
*/
@@ -90,23 +89,35 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
}
public abstract static class DvrGuidedStepDialogFragment extends DvrHalfSizedDialogFragment {
+ private DvrGuidedStepFragment mFragment;
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
- GuidedStepFragment fragment = onCreateGuidedStepFragment();
- fragment.setArguments(getArguments());
- GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ mFragment = onCreateGuidedStepFragment();
+ mFragment.setArguments(getArguments());
+ mFragment.setOnActionClickListener(getOnActionClickListener());
+ GuidedStepFragment.add(getChildFragmentManager(),
+ mFragment, R.id.halfsized_dialog_host);
return view;
}
- protected abstract GuidedStepFragment onCreateGuidedStepFragment();
+ @Override
+ public void setOnActionClickListener(OnActionClickListener listener) {
+ super.setOnActionClickListener(listener);
+ if (mFragment != null) {
+ mFragment.setOnActionClickListener(listener);
+ }
+ }
+
+ protected abstract DvrGuidedStepFragment onCreateGuidedStepFragment();
}
/** A dialog fragment for {@link DvrScheduleFragment}. */
public static class DvrScheduleDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrScheduleFragment();
}
}
@@ -114,7 +125,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** A dialog fragment for {@link DvrProgramConflictFragment}. */
public static class DvrProgramConflictDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrProgramConflictFragment();
}
}
@@ -122,7 +133,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** A dialog fragment for {@link DvrChannelWatchConflictFragment}. */
public static class DvrChannelWatchConflictDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrChannelWatchConflictFragment();
}
}
@@ -131,7 +142,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
public static class DvrChannelRecordDurationOptionDialogFragment
extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrChannelRecordDurationOptionFragment();
}
}
@@ -140,7 +151,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
public static class DvrInsufficientSpaceErrorDialogFragment
extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrInsufficientSpaceErrorFragment();
}
}
@@ -149,15 +160,52 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
public static class DvrMissingStorageErrorDialogFragment
extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrMissingStorageErrorFragment();
}
}
+ /**
+ * A dialog fragment to show error message when the current storage is too small to
+ * support DVR
+ */
+ public static class DvrSmallSizedStorageErrorDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrGuidedStepFragment() {
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(
+ R.string.dvr_error_small_sized_storage_title);
+ String description = getResources().getString(
+ R.string.dvr_error_small_sized_storage_description,
+ DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024
+ / 1024 / 1024);
+ return new Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(GuidedAction.ACTION_ID_OK)
+ .title(android.R.string.ok)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ dismissDialog();
+ }
+ };
+ }
+ }
+
/** A dialog fragment for {@link DvrStopRecordingFragment}. */
public static class DvrStopRecordingDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrStopRecordingFragment();
}
}
@@ -165,7 +213,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** A dialog fragment for {@link DvrAlreadyScheduledFragment}. */
public static class DvrAlreadyScheduledDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrAlreadyScheduledFragment();
}
}
@@ -173,8 +221,8 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
/** A dialog fragment for {@link DvrAlreadyRecordedFragment}. */
public static class DvrAlreadyRecordedDialogFragment extends DvrGuidedStepDialogFragment {
@Override
- protected GuidedStepFragment onCreateGuidedStepFragment() {
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
return new DvrAlreadyRecordedFragment();
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java
new file mode 100644
index 00000000..339e5d2f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrItemPresenter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.android.tv.dvr.DvrUiHelper;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in
+ * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording},
+ * {@link RecordedProgram}, and {@link SeriesRecording}.
+ */
+public abstract class DvrItemPresenter extends Presenter {
+ private final Set<ViewHolder> mBoundViewHolders = new HashSet<>();
+ private final OnClickListener mOnClickListener = onCreateOnClickListener();
+
+ @Override
+ @CallSuper
+ public void onBindViewHolder(ViewHolder viewHolder, Object o) {
+ viewHolder.view.setTag(o);
+ viewHolder.view.setOnClickListener(mOnClickListener);
+ mBoundViewHolders.add(viewHolder);
+ }
+
+ @Override
+ @CallSuper
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ mBoundViewHolders.remove(viewHolder);
+ }
+
+ /**
+ * Unbinds all bound view holders.
+ */
+ public void unbindAllViewHolders() {
+ // When browse fragments are destroyed, RecyclerView would not call presenters'
+ // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks.
+ for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) {
+ onUnbindViewHolder(viewHolder);
+ }
+ }
+
+ /**
+ * Creates {@link OnClickListener} for DVR library's card views.
+ */
+ protected OnClickListener onCreateOnClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (view instanceof RecordingCardView) {
+ RecordingCardView v = (RecordingCardView) view;
+ DvrUiHelper.startDetailsActivity((Activity) v.getContext(),
+ v.getTag(), v.getImageView(), false);
+ }
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java
index 7be92f1e..8c4c856c 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java
@@ -22,6 +22,7 @@ import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.android.tv.R;
@@ -36,8 +37,6 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
private static final String TAG = "DvrPlaybackCardPresenter";
private static final boolean DEBUG = false;
- private int mSelectedBackgroundColor = -1;
- private int mDefaultBackgroundColor = -1;
private final int mRelatedRecordingCardWidth;
private final int mRelatedRecordingCardHeight;
@@ -58,12 +57,17 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
}
@Override
- public void onClick(View v) {
- long programId = (long) v.getTag();
- if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId);
- Intent intent = new Intent(getContext(), DvrPlaybackActivity.class);
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
- getContext().startActivity(intent);
+ protected OnClickListener onCreateOnClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ long programId = ((RecordedProgram) v.getTag()).getId();
+ if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId);
+ Intent intent = new Intent(getContext(), DvrPlaybackActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
+ getContext().startActivity(intent);
+ }
+ };
}
@Override
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
index 1a3ae43c..0bc4ecb1 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
@@ -126,6 +126,13 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (mReadyToControl) {
+ if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN
+ && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
+ || mPlaybackState == PlaybackState.STATE_REWINDING)) {
+ // Workaround of b/31489271. Clicks play/pause button first to reset play controls
+ // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused.
+ onActionClicked(getControlsRow().getActionForKeyCode(keyCode));
+ }
return super.onKey(v, keyCode, event);
}
return false;
@@ -134,10 +141,7 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
@Override
public boolean hasValidMedia() {
PlaybackState playbackState = mMediaController.getPlaybackState();
- if (playbackState == null) {
- return false;
- }
- return true;
+ return playbackState != null;
}
@Override
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
index 9184f4f7..51ec93b8 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
@@ -33,6 +33,7 @@ import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
@@ -42,6 +43,7 @@ import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.data.BaseProgram;
import com.android.tv.dvr.RecordedProgram;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dvr.DvrDataManager;
@@ -63,7 +65,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
private DvrPlaybackMediaSessionHelper mMediaSessionHelper;
private DvrPlaybackControlHelper mPlaybackControlHelper;
private ArrayObjectAdapter mRowsAdapter;
- private ArrayObjectAdapter mRelatedRecordingsRowAdapter;
+ private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter;
+ private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
private DvrDataManager mDvrDataManager;
private ContentRatingsManager mContentRatingsManager;
private TvView mTvView;
@@ -108,7 +111,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view);
mBlockScreenView = getActivity().findViewById(R.id.block_screen);
mMediaSessionHelper = new DvrPlaybackMediaSessionHelper(
- getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView));
+ getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this);
mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this);
setUpRows();
preparePlayback(getActivity().getIntent());
@@ -166,9 +169,10 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
@Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
- super.onDestroy();
mPlaybackControlHelper.unregisterCallback();
mMediaSessionHelper.release();
+ mRelatedRecordingCardPresenter.unbindAllViewHolders();
+ super.onDestroy();
}
/**
@@ -196,6 +200,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
updateAspectRatio(mAppliedAspectRatio);
}
+ public RecordedProgram getNextEpisode(RecordedProgram program) {
+ int position = mRelatedRecordingsRowAdapter.findInsertPosition(program);
+ if (position == mRelatedRecordingsRowAdapter.size()) {
+ return null;
+ } else {
+ return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position);
+ }
+ }
+
void onMediaControllerUpdated() {
mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
}
@@ -261,8 +274,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
}
private ListRow getRelatedRecordingsRow() {
- mRelatedRecordingsRowAdapter =
- new ArrayObjectAdapter(new DvrPlaybackCardPresenter(getActivity()));
+ mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity());
+ mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter);
HeaderItem header = new HeaderItem(0,
getActivity().getString(R.string.dvr_playback_related_recordings));
return new ListRow(header, mRelatedRecordingsRowAdapter);
@@ -277,4 +290,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME,
TvInputManager.TIME_SHIFT_INVALID_TIME);
}
+
+ private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
+ RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
+ super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
+ }
+
+ @Override
+ long getId(BaseProgram item) {
+ return item.getId();
+ }
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index a907b198..da6d1637 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -17,7 +17,6 @@
package com.android.tv.dvr.ui;
import android.annotation.TargetApi;
-import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
@@ -27,7 +26,6 @@ import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.text.format.DateUtils;
-import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
@@ -37,10 +35,10 @@ import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.util.Utils;
+import java.util.Collections;
import java.util.List;
/**
@@ -50,6 +48,8 @@ import java.util.List;
*/
@TargetApi(Build.VERSION_CODES.N)
public class DvrScheduleFragment extends DvrGuidedStepFragment {
+ private static final String TAG = "DvrScheduleFragment";
+
private static final int ACTION_RECORD_EPISODE = 1;
private static final int ACTION_RECORD_SERIES = 2;
@@ -62,8 +62,12 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
}
DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
- SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic()
- && dvrManager.getSeriesRecording(mProgram) == null);
+ SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG,
+ "The program should be episodic: " + mProgram);
+ SeriesRecording seriesRecording = dvrManager.getSeriesRecording(mProgram);
+ SoftPreconditions.checkArgument(seriesRecording == null
+ || seriesRecording.isStopped(), TAG,
+ "The series recording should be stopped or null: " + seriesRecording);
super.onCreate(savedInstanceState);
}
@@ -122,19 +126,22 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
R.id.halfsized_dialog_host);
}
} else if (action.getId() == ACTION_RECORD_SERIES) {
- ProgressDialog dialog = ProgressDialog.show(getContext(), null,
- getString(R.string.dvr_schedule_progress_message_reading_programs));
- getDvrManager().queryProgramsForSeries(mProgram, new ProgramLoadCallback() {
- @Override
- public void onProgramLoadFinished(@NonNull List<Program> programs) {
- dialog.dismiss();
- // TODO: Create series recording in series settings fragment.
- SeriesRecording seriesRecording =
- getDvrManager().addSeriesRecording(mProgram, programs);
- DvrUiHelper.startSeriesSettingsActivity(getContext(), seriesRecording.getId());
- dismissDialog();
- }
- });
+ SeriesRecording seriesRecording = TvApplication.getSingletons(getContext())
+ .getDvrDataManager().getSeriesRecording(mProgram.getSeriesId());
+ if (seriesRecording == null) {
+ seriesRecording = getDvrManager().addSeriesRecording(mProgram,
+ Collections.emptyList(), SeriesRecording.STATE_SERIES_STOPPED);
+ } else {
+ // Reset priority to the highest.
+ seriesRecording = SeriesRecording.buildFrom(seriesRecording)
+ .setPriority(TvApplication.getSingletons(getContext())
+ .getDvrScheduleManager().suggestNewSeriesPriority())
+ .build();
+ getDvrManager().updateSeriesRecording(seriesRecording);
+ }
+ DvrUiHelper.startSeriesSettingsActivity(getContext(),
+ seriesRecording.getId(), null, true, true, true);
+ dismissDialog();
}
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
index 316cb381..f6e6ac26 100644
--- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
@@ -17,16 +17,24 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.app.ProgressDialog;
import android.os.Bundle;
import android.support.annotation.IntDef;
-import android.support.v17.leanback.app.DetailsFragment;
import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.EpisodicProgramLoadTask;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.SeriesRecordingScheduler;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* Activity to show the list of recording schedules.
@@ -49,46 +57,48 @@ public class DvrSchedulesActivity extends Activity {
* A type which means the activity will display a scheduled recording list of a series
* recording.
*/
- public final static int TYPE_SERIES_SCHEDULE = 1;
-
- private Runnable mCancelAllClickedRunnable;
+ public static final int TYPE_SERIES_SCHEDULE = 1;
@Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ public void onCreate(final Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
+ // Pass null to prevent automatically re-creating fragments
+ super.onCreate(null);
setContentView(R.layout.activity_dvr_schedules);
- if (savedInstanceState == null) {
- int schedulesType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE);
- DetailsFragment schedulesFragment = null;
- if (schedulesType == TYPE_FULL_SCHEDULE) {
- schedulesFragment = new DvrSchedulesFragment();
- schedulesFragment.setArguments(getIntent().getExtras());
- } else if (schedulesType == TYPE_SERIES_SCHEDULE) {
- schedulesFragment = new DvrSeriesSchedulesFragment();
- schedulesFragment.setArguments(getIntent().getExtras());
- }
- if (schedulesFragment != null) {
- getFragmentManager().beginTransaction().add(
- R.id.fragment_container, schedulesFragment).commit();
- } else {
- finish();
- }
- }
- }
-
- /**
- * Sets cancel all runnable which will implement operations after clicking cancel all dialog.
- */
- public void setCancelAllClickedRunnable(Runnable cancelAllClickedRunnable) {
- mCancelAllClickedRunnable = cancelAllClickedRunnable;
- }
-
- /**
- * Operations after clicking the cancel all.
- */
- public void onCancelAllClicked() {
- if (mCancelAllClickedRunnable != null) {
- mCancelAllClickedRunnable.run();
+ int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE);
+ if (scheduleType == TYPE_FULL_SCHEDULE) {
+ DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment();
+ schedulesFragment.setArguments(getIntent().getExtras());
+ getFragmentManager().beginTransaction().add(
+ R.id.fragment_container, schedulesFragment).commit();
+ } else if (scheduleType == TYPE_SERIES_SCHEDULE) {
+ final ProgressDialog dialog = ProgressDialog.show(this, null, getString(
+ R.string.dvr_series_schedules_progress_message_reading_programs));
+ SeriesRecording seriesRecording = getIntent().getExtras()
+ .getParcelable(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ // To get programs faster, hold the update of the series schedules.
+ SeriesRecordingScheduler.getInstance(this).pauseUpdate();
+ new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate();
+ dialog.dismiss();
+ Bundle args = getIntent().getExtras();
+ args.putParcelableArrayList(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs));
+ DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment();
+ schedulesFragment.setArguments(args);
+ getFragmentManager().beginTransaction().add(
+ R.id.fragment_container, schedulesFragment).commit();
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true)
+ .execute();
+ } else {
+ finish();
}
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index ab695234..f57e4b05 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -37,6 +37,7 @@ public class DvrSeriesDeletionActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_series_settings);
// Check savedInstanceState to prevent that activity is being showed with animation.
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java
new file mode 100644
index 00000000..1a0d13d3
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.app.GuidedStepFragment;
+
+import com.android.tv.R;
+
+public class DvrSeriesScheduledDialogActivity extends Activity {
+ /**
+ * Name of series recording id added to the Intent.
+ */
+ public static final String SERIES_RECORDING_ID = "series_recording_id";
+
+ /**
+ * Name of flag to check if the dialog should show view schedule option.
+ */
+ public static final String SHOW_VIEW_SCHEDULE_OPTION = "show_view_schedule_option";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.halfsized_dialog);
+ if (savedInstanceState == null) {
+ DvrSeriesScheduledFragment dvrSeriesScheduledFragment =
+ new DvrSeriesScheduledFragment();
+ dvrSeriesScheduledFragment.setArguments(getIntent().getExtras());
+ GuidedStepFragment.addAsRoot(this, dvrSeriesScheduledFragment,
+ R.id.halfsized_dialog_host);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
new file mode 100644
index 00000000..1173df46
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+
+import java.util.List;
+
+public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
+ private final static long SERIES_RECORDING_ID_NOT_SET = -1;
+
+ private final static int ACTION_VIEW_SCHEDULES = 1;
+
+ private DvrScheduleManager mDvrScheduleManager;
+ private SeriesRecording mSeriesRecording;
+ private boolean mShowViewScheduleOption;
+
+ private int mSchedulesAddedCount = 0;
+ private boolean mHasConflict = false;
+ private int mInThisSeriesConflictCount = 0;
+ private int mOutThisSeriesConflictCount = 0;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ long seriesRecordingId = getArguments().getLong(
+ DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, SERIES_RECORDING_ID_NOT_SET);
+ if (seriesRecordingId == SERIES_RECORDING_ID_NOT_SET) {
+ getActivity().finish();
+ return;
+ }
+ mShowViewScheduleOption = getArguments().getBoolean(
+ DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION);
+ mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager();
+ mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager()
+ .getSeriesRecording(seriesRecordingId);
+ if (mSeriesRecording == null) {
+ getActivity().finish();
+ return;
+ }
+ mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager()
+ .getAvailableScheduledRecording(mSeriesRecording.getId()).size();
+ List<ScheduledRecording> conflictingRecordings =
+ mDvrScheduleManager.getConflictingSchedules(mSeriesRecording);
+ mHasConflict = !conflictingRecordings.isEmpty();
+ for (ScheduledRecording recording : conflictingRecordings) {
+ if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ ++mInThisSeriesConflictCount;
+ } else {
+ ++mOutThisSeriesConflictCount;
+ }
+ }
+ }
+
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.dvr_series_recording_dialog_title);
+ Drawable icon;
+ if (!mHasConflict) {
+ icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null);
+ } else {
+ icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
+ }
+ return new GuidanceStylist.Guidance(title, getDescription(), null, icon);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Context context = getContext();
+ actions.add(new GuidedAction.Builder(context)
+ .clickAction(GuidedAction.ACTION_ID_OK)
+ .build());
+ if (mShowViewScheduleOption) {
+ actions.add(new GuidedAction.Builder(context)
+ .id(ACTION_VIEW_SCHEDULES)
+ .title(R.string.dvr_action_view_schedules)
+ .build());
+ }
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_VIEW_SCHEDULES) {
+ Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class);
+ intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity
+ .TYPE_SERIES_SCHEDULE);
+ intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING,
+ mSeriesRecording);
+ startActivity(intent);
+ }
+ getActivity().finish();
+ }
+
+ private String getDescription() {
+ if (!mHasConflict) {
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount,
+ mSchedulesAddedCount, mSeriesRecording.getTitle());
+ } else {
+ // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means
+ // mHasConflict is false. So we don't need to check that case.
+ if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) {
+ return getResources().getQuantityString(R.plurals
+ .dvr_series_recording_scheduled_this_and_other_series_conflict,
+ mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
+ mInThisSeriesConflictCount + mOutThisSeriesConflictCount);
+ } else if (mInThisSeriesConflictCount != 0) {
+ return getResources().getQuantityString(R.plurals
+ .dvr_series_recording_scheduled_only_this_series_conflict,
+ mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
+ mInThisSeriesConflictCount);
+ } else {
+ if (mOutThisSeriesConflictCount == 1) {
+ return getResources().getQuantityString(R.plurals
+ .dvr_series_recording_scheduled_only_other_series_one_conflict,
+ mSchedulesAddedCount, mSchedulesAddedCount,
+ mSeriesRecording.getTitle());
+ } else {
+ return getResources().getQuantityString(R.plurals
+ .dvr_series_recording_scheduled_only_other_series_conflict,
+ mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
+ mOutThisSeriesConflictCount);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
index 2af78081..3f7671b3 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -17,13 +17,14 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.ui.sidepanel.SettingsFragment;
/**
* Activity to show details view in DVR.
@@ -31,22 +32,51 @@ import com.android.tv.ui.sidepanel.SettingsFragment;
public class DvrSeriesSettingsActivity extends Activity {
/**
* Name of series id added to the Intent.
+ * Type: Long
*/
public static final String SERIES_RECORDING_ID = "series_recording_id";
+ /**
+ * Name of the boolean flag to decide if the series recording with empty schedule and recording
+ * will be removed.
+ */
+ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording";
+ /**
+ * Name of the boolean flag to decide if the setting fragment should be translucent.
+ */
+ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent";
+ /**
+ * Name of the channel id list. If the channel list is given, we show the channels
+ * from the values in channel option.
+ * Type: Long array
+ */
+ public static final String CHANNEL_ID_LIST = "channel_id_list";
+
+ /**
+ * Name of the boolean flag to check if the confirm dialog should show view schedule option.
+ */
+ public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG =
+ "show_view_schedule_option_in_dialog";
@Override
public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dvr_series_settings);
long seriesRecordingId = getIntent().getLongExtra(SERIES_RECORDING_ID, -1);
SoftPreconditions.checkArgument(seriesRecordingId != -1);
if (savedInstanceState == null) {
- Bundle args = new Bundle();
- args.putLong(SeriesSettingsFragment.SERIES_RECORDING_ID, seriesRecordingId);
SeriesSettingsFragment settingFragment = new SeriesSettingsFragment();
- settingFragment.setArguments(args);
+ settingFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame);
}
}
-}
+
+ @Override
+ public void onAttachedToWindow() {
+ if (!getIntent().getExtras().getBoolean(IS_WINDOW_TRANSLUCENT, true)) {
+ getWindow().setBackgroundDrawable(
+ new ColorDrawable(getColor(R.color.common_tv_background)));
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index c0e21a18..c3867886 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -21,16 +21,22 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
+import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.ScheduledRecording;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -41,10 +47,33 @@ import java.util.List;
*/
@TargetApi(Build.VERSION_CODES.N)
public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
- private static final int ACTION_STOP = 1;
+ /**
+ * The action ID for the stop action.
+ */
+ public static final int ACTION_STOP = 1;
+ /**
+ * Key for the program.
+ * Type: {@link com.android.tv.data.Program}.
+ */
+ public static final String KEY_REASON = "DvrStopRecordingFragment.type";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_USER_STOP, REASON_ON_CONFLICT})
+ public @interface ReasonType {}
+ /**
+ * The dialog is shown because users want to stop some currently recording program.
+ */
+ public static final int REASON_USER_STOP = 1;
+ /**
+ * The dialog is shown because users want to record some program that is conflict to the
+ * current recording program.
+ */
+ public static final int REASON_ON_CONFLICT = 2;
private ScheduledRecording mSchedule;
private DvrDataManager mDvrDataManager;
+ private @ReasonType int mStopReason;
+
private final ScheduledRecordingListener mScheduledRecordingListener =
new ScheduledRecordingListener() {
@Override
@@ -85,6 +114,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
}
mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
+ mStopReason = args.getInt(KEY_REASON);
}
@Override
@@ -99,7 +129,20 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
@Override
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title = getString(R.string.dvr_stop_recording_dialog_title);
- String description = getString(R.string.dvr_stop_recording_dialog_description);
+ String description;
+ if (mStopReason == REASON_ON_CONFLICT) {
+ String programTitle = mSchedule.getProgramTitle();
+ if (TextUtils.isEmpty(programTitle)) {
+ ChannelDataManager channelDataManager =
+ TvApplication.getSingletons(getActivity()).getChannelDataManager();
+ Channel channel = channelDataManager.getChannel(mSchedule.getChannelId());
+ programTitle = channel.getDisplayName();
+ }
+ description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict,
+ mSchedule.getProgramTitle());
+ } else {
+ description = getString(R.string.dvr_stop_recording_dialog_description);
+ }
Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null);
return new Guidance(title, description, null, image);
}
@@ -115,12 +158,4 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
.clickAction(GuidedAction.ACTION_ID_CANCEL)
.build());
}
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_STOP) {
- getDvrManager().stopRecording(mSchedule);
- }
- dismissDialog();
- }
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java
index d1cf57a6..5b880bd6 100644
--- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java
@@ -26,16 +26,16 @@ import android.view.ViewGroup;
import com.android.tv.R;
/**
- * A dialog fragment which contains {@link DvrCancelAllSeriesRecordingFragment}.
+ * A dialog fragment which contains {@link DvrStopSeriesRecordingFragment}.
*/
-public class DvrCancelAllSeriesRecordingDialogFragment extends DialogFragment {
+public class DvrStopSeriesRecordingDialogFragment extends DialogFragment {
public static final String DIALOG_TAG = "dialog_tag";
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.halfsized_dialog, container, false);
- GuidedStepFragment fragment = new DvrCancelAllSeriesRecordingFragment();
+ GuidedStepFragment fragment = new DvrStopSeriesRecordingFragment();
fragment.setArguments(getArguments());
GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host);
return view;
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
new file mode 100644
index 00000000..feaa2357
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.SeriesRecording;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment which asks the user to stop series recording.
+ */
+public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment {
+ /**
+ * Key for the series recording to be stopped.
+ */
+ public static final String KEY_SERIES_RECORDING = "key_series_recoridng";
+
+ private static final int ACTION_STOP_SERIES_RECORDING = 1;
+
+ private SeriesRecording mSeriesRecording;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mSeriesRecording = getArguments().getParcelable(KEY_SERIES_RECORDING);
+ return super.onCreateView(inflater, container, savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.dvr_series_schedules_stop_dialog_title);
+ String description = getString(R.string.dvr_series_schedules_stop_dialog_description);
+ Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete);
+ return new GuidanceStylist.Guidance(title, description, null, icon);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_STOP_SERIES_RECORDING)
+ .title(R.string.dvr_series_schedules_stop_dialog_action_stop)
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .clickAction(GuidedAction.ACTION_ID_CANCEL)
+ .build());
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_STOP_SERIES_RECORDING) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ DvrManager dvrManager = singletons.getDvrManager();
+ DvrDataManager dataManager = singletons.getDvrDataManager();
+ List<ScheduledRecording> toDelete = new ArrayList<>();
+ for (ScheduledRecording r : dataManager.getAvailableScheduledRecordings()) {
+ if (r.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ toDelete.add(r);
+ } else {
+ dvrManager.stopRecording(r);
+ }
+ }
+ }
+ if (!toDelete.isEmpty()) {
+ dvrManager.forceRemoveScheduledRecording(ScheduledRecording.toArray(toDelete));
+ }
+ dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording)
+ .setState(SeriesRecording.STATE_SERIES_STOPPED).build());
+ }
+ dismissDialog();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
index fcf0925b..d320816e 100644
--- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
@@ -35,6 +35,8 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30);
+ private OnActionClickListener mOnActionClickListener;
+
private Handler mHandler = new Handler();
private Runnable mAutoDismisser = new Runnable() {
@Override
@@ -63,6 +65,16 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
}
@Override
+ public void onPause() {
+ super.onPause();
+ if (mOnActionClickListener != null) {
+ // Dismisses the dialog to prevent the callback being forgotten during
+ // fragment re-creating.
+ dismiss();
+ }
+ }
+
+ @Override
public void onStop() {
super.onStop();
mHandler.removeCallbacks(mAutoDismisser);
@@ -77,4 +89,29 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
public String getTrackerLabel() {
return TRACKER_LABEL;
}
+
+ /**
+ * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog
+ * will be automatically closed when it's paused to prevent the fragment being re-created by
+ * the framework, which will result the listener being forgotten.
+ */
+ public void setOnActionClickListener(OnActionClickListener listener) {
+ mOnActionClickListener = listener;
+ }
+
+ /**
+ * Returns {@link OnActionClickListener} for sub-classes or any inner fragments.
+ */
+ protected OnActionClickListener getOnActionClickListener() {
+ return mOnActionClickListener;
+ }
+
+ /**
+ * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments
+ * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier
+ * of the action user clicked.
+ */
+ public interface OnActionClickListener {
+ void onActionClick(long actionId);
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
index 9f78985f..158bd824 100644
--- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
@@ -52,7 +52,6 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
// button action's IDs are negative.
private static final long ACTION_ID_SAVE = -100L;
- private DvrDataManager mDvrDataManager;
private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>();
private SeriesRecording mSelectedRecording;
@@ -64,23 +63,23 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
mSeriesRecordings.clear();
mSeriesRecordings.add(new SeriesRecording.Builder()
.setTitle(getString(R.string.dvr_priority_action_one_time_recording))
.setPriority(Long.MAX_VALUE)
.setId(ONE_TIME_RECORDING_ID)
.build());
+ DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
long comeFromSeriesRecordingId =
getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1);
- for (SeriesRecording series : mDvrDataManager.getSeriesRecordings()) {
+ for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) {
if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL
|| series.getId() == comeFromSeriesRecordingId) {
mSeriesRecordings.add(series);
}
}
mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR);
- mComeFromSeriesRecording = mDvrDataManager.getSeriesRecording(comeFromSeriesRecordingId);
+ mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId);
mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal);
mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null);
mSelectedActionColor =
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
index 9eb7e385..e698b8a2 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
@@ -16,11 +16,8 @@
package com.android.tv.dvr.ui;
-import android.content.Intent;
import android.content.res.Resources;
-import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
-import android.net.Uri;
import android.os.Bundle;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.OnActionClickedListener;
@@ -30,40 +27,38 @@ import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
-import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrPlaybackActivity;
-import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.parental.ParentalControlSettings;
-import com.android.tv.util.TvInputManagerHelper;
-import com.android.tv.util.Utils;
-
-import java.io.File;
/**
* {@link DetailsFragment} for recorded program in DVR.
*/
-public class RecordedProgramDetailsFragment extends DvrDetailsFragment {
+public class RecordedProgramDetailsFragment extends DvrDetailsFragment
+ implements DvrDataManager.RecordedProgramListener {
private static final int ACTION_RESUME_PLAYING = 1;
private static final int ACTION_PLAY_FROM_BEGINNING = 2;
private static final int ACTION_DELETE_RECORDING = 3;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
- private TvInputManagerHelper mTvInputManagerHelper;
private RecordedProgram mRecordedProgram;
private DetailsContent mDetailsContent;
private boolean mPaused;
+ private DvrDataManager mDvrDataManager;
@Override
public void onCreate(Bundle savedInstanceState) {
+ mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
+ mDvrDataManager.addRecordedProgramListener(this);
super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onCreateInternal() {
mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
.getDvrWatchedPositionManager();
- mTvInputManagerHelper = TvApplication.getSingletons(getActivity())
- .getTvInputManagerHelper();
setDetailsOverviewRow(mDetailsContent);
}
@@ -83,10 +78,15 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment {
}
@Override
+ public void onDestroy() {
+ mDvrDataManager.removeRecordedProgramListener(this);
+ super.onDestroy();
+ }
+
+ @Override
protected boolean onLoadRecordingDetails(Bundle args) {
long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
- mRecordedProgram = TvApplication.getSingletons(getActivity()).getDvrDataManager()
- .getRecordedProgram(recordedProgramId);
+ mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId);
if (mRecordedProgram == null) {
// notify super class to end activity before initializing anything
return false;
@@ -114,8 +114,8 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment {
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
Resources res = getResources();
- if (mDvrWatchedPositionManager.getWatchedPosition(mRecordedProgram.getId())
- != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram)
+ == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING,
res.getString(R.string.dvr_detail_resume_play), null,
res.getDrawable(R.drawable.lb_ic_play)));
@@ -139,9 +139,9 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
- startPlayback(TvInputManager.TIME_SHIFT_INVALID_TIME);
+ startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME);
} else if (action.getId() == ACTION_RESUME_PLAYING) {
- startPlayback(mDvrWatchedPositionManager
+ startPlayback(mRecordedProgram, mDvrWatchedPositionManager
.getWatchedPosition(mRecordedProgram.getId()));
} else if (action.getId() == ACTION_DELETE_RECORDING) {
DvrManager dvrManager = TvApplication
@@ -153,66 +153,18 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment {
};
}
- private boolean isDataUriAccessible(Uri dataUri) {
- if (dataUri == null || dataUri.getPath() == null) {
- return false;
- }
- try {
- File recordedProgramPath = new File(dataUri.getPath());
- if (recordedProgramPath.exists()) {
- return true;
- }
- } catch (SecurityException e) {
- }
- return false;
- }
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { }
- private void startPlayback(long seekTimeMs) {
- if (Utils.isInBundledPackageSet(mRecordedProgram.getPackageName())
- && !isDataUriAccessible(mRecordedProgram.getDataUri())) {
- // Currently missing storage is handled only for TunerTvInput.
- DvrUiHelper.showDvrMissingStorageErrorDialog(getActivity(),
- mRecordedProgram.getInputId());
- return;
- }
- ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings();
- if (!parental.isParentalControlsEnabled()) {
- launchPlaybackActivity(seekTimeMs, false);
- return;
- }
- String ratingString = mRecordedProgram.getContentRating();
- if (TextUtils.isEmpty(ratingString)) {
- launchPlaybackActivity(seekTimeMs, false);
- return;
- }
- String[] ratingList = ratingString.split(",");
- TvContentRating[] programRatings = new TvContentRating[ratingList.length];
- for (int i = 0; i < ratingList.length; i++) {
- programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]);
- }
- TvContentRating blockRatings = parental.getBlockedRating(programRatings);
- if (blockRatings != null) {
- new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- if (success) {
- launchPlaybackActivity(seekTimeMs, true);
- }
- }
- }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
- } else {
- launchPlaybackActivity(seekTimeMs, false);
- }
- }
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { }
- private void launchPlaybackActivity(long seekTimeMs, boolean pinChecked) {
- Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class);
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId());
- if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs);
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (recordedProgram.getId() == mRecordedProgram.getId()) {
+ getActivity().finish();
+ }
}
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked);
- getActivity().startActivity(intent);
}
}
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
index 704d3a3f..1bf34310 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
+++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
@@ -21,13 +21,11 @@ import android.content.Context;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
import android.net.Uri;
-import android.support.v17.leanback.widget.Presenter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
import android.view.View;
-import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.android.tv.R;
@@ -35,7 +33,6 @@ import com.android.tv.TvApplication;
import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
import com.android.tv.util.Utils;
@@ -45,7 +42,7 @@ import java.util.concurrent.TimeUnit;
/**
* Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}.
*/
-public class RecordedProgramPresenter extends Presenter implements OnClickListener {
+public class RecordedProgramPresenter extends DvrItemPresenter {
private final ChannelDataManager mChannelDataManager;
private final DvrWatchedPositionManager mDvrWatchedPositionManager;
private final Context mContext;
@@ -108,20 +105,16 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen
public void onBindViewHolder(ViewHolder viewHolder, Object o) {
final RecordedProgram program = (RecordedProgram) o;
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- cardView.setTag(program);
Channel channel = mChannelDataManager.getChannel(program.getChannelId());
- SpannableString title;
- if (mShowEpisodeTitle) {
- title = new SpannableString(program.getEpisodeDisplayTitle(mContext));
- } else {
- String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(mContext);
- title = titleWithEpisodeNumber == null ? null
- : new SpannableString(titleWithEpisodeNumber);
- }
+ String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext)
+ : program.getTitleWithEpisodeNumber(mContext);
+ SpannableString title = titleString == null ? null : new SpannableString(titleString);
if (TextUtils.isEmpty(title)) {
title = new SpannableString(channel != null ? channel.getDisplayName()
: mContext.getResources().getString(R.string.no_program_information));
} else if (!mShowEpisodeTitle) {
+ // TODO: Some translation may add delimiters in-between program titles, we should use
+ // a more robust way to get the span range.
String programTitle = program.getTitle();
title.setSpan(new TextAppearanceSpan(mContext,
R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
@@ -144,7 +137,6 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen
String durationString = getContext().getResources().getQuantityString(
R.plurals.dvr_program_duration, durationMinutes, durationMinutes);
cardView.setContent(getDescription(program), durationString);
- viewHolder.view.setOnClickListener(this);
if (viewHolder instanceof RecordedProgramViewHolder) {
RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder;
cardViewHolder.setProgram(program);
@@ -152,6 +144,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen
cardViewHolder
.setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId()));
}
+ super.onBindViewHolder(viewHolder, o);
}
@Override
@@ -161,14 +154,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen
((RecordedProgramViewHolder) viewHolder).mProgram.getId());
}
((RecordingCardView) viewHolder.view).reset();
- }
-
- @Override
- public void onClick(View v) {
- if (v instanceof RecordingCardView) {
- DvrUiHelper.startDetailsActivity((Activity) mContext, (RecordedProgram) v.getTag(),
- ((RecordingCardView) v).getImageView());
- }
+ super.onUnbindViewHolder(viewHolder);
}
/**
diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java
index fa4562fd..51c3b03b 100644
--- a/src/com/android/tv/dvr/ui/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/RecordingCardView.java
@@ -48,6 +48,8 @@ class RecordingCardView extends BaseCardView {
private final TextView mMajorContentView;
private final TextView mMinorContentView;
private final ProgressBar mProgressBar;
+ private final View mAffiliatedIconContainer;
+ private final ImageView mAffiliatedIcon;
private final Drawable mDefaultImage;
RecordingCardView(Context context) {
@@ -71,6 +73,8 @@ class RecordingCardView extends BaseCardView {
mImageWidth = imageWidth;
mImageHeight = imageHeight;
mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
+ mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
+ mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
mTitleView = (TextView) findViewById(R.id.title);
mMajorContentView = (TextView) findViewById(R.id.content_major);
mMinorContentView = (TextView) findViewById(R.id.content_minor);
@@ -138,6 +142,15 @@ class RecordingCardView extends BaseCardView {
}
}
+ public void setAffiliatedIcon(int imageResId) {
+ if (imageResId > 0) {
+ mAffiliatedIconContainer.setVisibility(View.VISIBLE);
+ mAffiliatedIcon.setImageResource(imageResId);
+ } else {
+ mAffiliatedIconContainer.setVisibility(View.INVISIBLE);
+ }
+ }
+
/**
* Returns image view.
*/
diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
index 2271d932..4e19ec3f 100644
--- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
@@ -33,13 +33,10 @@ import com.android.tv.dvr.ScheduledRecording;
*/
abstract class RecordingDetailsFragment extends DvrDetailsFragment {
private ScheduledRecording mRecording;
- private DetailsContent mDetailsContent;
@Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mDetailsContent = createDetailsContent();
- setDetailsOverviewRow(mDetailsContent);
+ protected void onCreateInternal() {
+ setDetailsOverviewRow(createDetailsContent());
}
@Override
@@ -47,11 +44,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment {
long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager()
.getScheduledRecording(scheduledRecordingId);
- if (mRecording == null) {
- // notify super class to end activity before initializing anything
- return false;
- }
- return true;
+ return mRecording != null;
}
/**
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
index 5c1ba48c..60816bb5 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
@@ -89,7 +89,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
private int getScheduleIconId() {
if (mDvrManager.isConflicting(getRecording())) {
- return R.drawable.ic_warning_white_36dp;
+ return R.drawable.ic_warning_white_32dp;
} else {
return R.drawable.ic_schedule_32dp;
}
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
index 1f67bbe3..5f447f13 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
@@ -20,12 +20,10 @@ import android.app.Activity;
import android.content.Context;
import android.media.tv.TvContract;
import android.os.Handler;
-import android.support.v17.leanback.widget.Presenter;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
-import android.view.View;
import android.view.ViewGroup;
import com.android.tv.ApplicationSingletons;
@@ -33,7 +31,7 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.util.Utils;
@@ -42,10 +40,11 @@ import java.util.concurrent.TimeUnit;
/**
* Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
*/
-public class ScheduledRecordingPresenter extends Presenter {
+public class ScheduledRecordingPresenter extends DvrItemPresenter {
private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
private final ChannelDataManager mChannelDataManager;
+ private final DvrManager mDvrManager;
private final Context mContext;
private final int mProgressBarColor;
@@ -95,6 +94,7 @@ public class ScheduledRecordingPresenter extends Presenter {
public ScheduledRecordingPresenter(Context context) {
ApplicationSingletons singletons = TvApplication.getSingletons(context);
mChannelDataManager = singletons.getChannelDataManager();
+ mDvrManager = singletons.getDvrManager();
mContext = context;
mProgressBarColor = context.getResources()
.getColor(R.color.play_controls_recording_icon_color_on_focus);
@@ -129,19 +129,15 @@ public class ScheduledRecordingPresenter extends Presenter {
cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(),
recording.getStartTimeMs(), false, true, false, 0), null);
}
+ if (mDvrManager.isConflicting(recording)) {
+ cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
+ } else {
+ cardView.setAffiliatedIcon(0);
+ }
viewHolder.updateProgressBar();
- View.OnClickListener clickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (v instanceof RecordingCardView) {
- DvrUiHelper.startDetailsActivity((Activity) v.getContext(), recording,
- ((RecordingCardView) v).getImageView(), false);
- }
- }
- };
- baseHolder.view.setOnClickListener(clickListener);
viewHolder.mScheduledRecording = recording;
viewHolder.startUpdateProgressBar();
+ super.onBindViewHolder(viewHolder, o);
}
@Override
@@ -151,6 +147,7 @@ public class ScheduledRecordingPresenter extends Presenter {
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
viewHolder.mScheduledRecording = null;
cardView.reset();
+ super.onUnbindViewHolder(viewHolder);
}
private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) {
diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
index c29d62ae..36e3cfc1 100644
--- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
@@ -37,6 +37,7 @@ import com.android.tv.dvr.RecordedProgram;
import com.android.tv.dvr.SeriesRecording;
import com.android.tv.ui.GuidedActionsStylistWithDivider;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -145,17 +146,19 @@ public class SeriesDeletionFragment extends GuidedStepFragment {
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == ACTION_ID_DELETE) {
- int deletionCount = 0;
- DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
+ List<Long> idsToDelete = new ArrayList<>();
for (GuidedAction guidedAction : getActions()) {
if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
&& guidedAction.isChecked()) {
- dvrManager.removeRecordedProgram(guidedAction.getId());
- deletionCount++;
+ idsToDelete.add(guidedAction.getId());
}
}
+ if (!idsToDelete.isEmpty()) {
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
+ dvrManager.removeRecordedPrograms(idsToDelete);
+ }
Toast.makeText(getContext(), getResources().getQuantityString(
- R.plurals.dvr_msg_episodes_deleted, deletionCount, deletionCount,
+ R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(),
mRecordings.size()), Toast.LENGTH_LONG).show();
finishGuidedStepFragments();
} else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
index 0156e9d9..e9e391d4 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
@@ -17,6 +17,8 @@
package com.android.tv.dvr.ui;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvInputManager;
import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
import android.support.v17.leanback.widget.Action;
@@ -37,8 +39,11 @@ import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.SeriesRecording;
import java.util.Collections;
@@ -50,24 +55,44 @@ import java.util.List;
*/
public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements
DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener {
- private static final int ACTION_SERIES_SCHEDULES = 1;
- private static final int ACTION_DELETE = 2;
+ private static final int ACTION_WATCH = 1;
+ private static final int ACTION_SERIES_SCHEDULES = 2;
+ private static final int ACTION_DELETE = 3;
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
private DvrDataManager mDvrDataManager;
private SeriesRecording mSeries;
// NOTICE: mRecordedPrograms should only be used in creating details fragments.
// After fragments are created, it should be cleared to save resources.
private List<RecordedProgram> mRecordedPrograms;
+ private RecordedProgram mRecommendRecordedProgram;
private DetailsContent mDetailsContent;
private int mSeasonRowCount;
private SparseArrayObjectAdapter mActionsAdapter;
private Action mDeleteAction;
+ private boolean mPaused;
+ private long mInitialPlaybackPositionMs;
+ private String mWatchLabel;
+ private String mResumeLabel;
+ private Drawable mWatchDrawable;
+ private RecordedProgramPresenter mRecordedProgramPresenter;
+
@Override
public void onCreate(Bundle savedInstanceState) {
mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
+ mWatchLabel = getString(R.string.dvr_detail_watch);
+ mResumeLabel = getString(R.string.dvr_detail_series_resume);
+ mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null);
+ mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true);
super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
+ .getDvrWatchedPositionManager();
setDetailsOverviewRow(mDetailsContent);
setupRecordedProgramsRow();
mDvrDataManager.addSeriesRecordingListener(this);
@@ -76,6 +101,45 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
}
@Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateWatchAction();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
+ }
+
+ private void updateWatchAction() {
+ List<RecordedProgram> programs = mDvrDataManager.getRecordedPrograms(mSeries.getId());
+ Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR);
+ mRecommendRecordedProgram = getRecommendProgram(programs);
+ if (mRecommendRecordedProgram == null) {
+ mActionsAdapter.clear(ACTION_WATCH);
+ } else {
+ String episodeStatus;
+ if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram)
+ == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ episodeStatus = mResumeLabel;
+ mInitialPlaybackPositionMs = mDvrWatchedPositionManager
+ .getWatchedPosition(mRecommendRecordedProgram.getId());
+ } else {
+ episodeStatus = mWatchLabel;
+ mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
+ }
+ String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber(
+ getContext());
+ mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH,
+ episodeStatus, episodeDisplayNumber, mWatchDrawable));
+ }
+ }
+
+ @Override
protected boolean onLoadRecordingDetails(Bundle args) {
long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager()
@@ -114,6 +178,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector());
Resources res = getResources();
+ updateWatchAction();
mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES,
getString(R.string.dvr_detail_view_schedule), null,
res.getDrawable(R.drawable.ic_schedule_32dp, null)));
@@ -137,11 +202,13 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
super.onDestroy();
mDvrDataManager.removeSeriesRecordingListener(this);
mDvrDataManager.removeRecordedProgramListener(this);
- if (mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED
- && mDvrDataManager.getRecordedPrograms(mSeries.getId()).isEmpty()) {
- TvApplication.getSingletons(getActivity()).getDvrManager()
- .removeSeriesRecording(mSeries.getId());
+ if (mSeries != null) {
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
+ if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) {
+ dvrManager.removeSeriesRecording(mSeries.getId());
+ }
}
+ mRecordedProgramPresenter.unbindAllViewHolders();
}
@Override
@@ -149,7 +216,9 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
return new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
- if (action.getId() == ACTION_SERIES_SCHEDULES) {
+ if (action.getId() == ACTION_WATCH) {
+ startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs);
+ } else if (action.getId() == ACTION_SERIES_SCHEDULES) {
DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries);
} else if (action.getId() == ACTION_DELETE) {
DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId());
@@ -158,6 +227,28 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
};
}
+ /**
+ * The programs are sorted by season number and episode number.
+ */
+ private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) {
+ for (int i = programs.size() - 1 ; i >= 0 ; i--) {
+ RecordedProgram program = programs.get(i);
+ int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program);
+ if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) {
+ continue;
+ }
+ if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ return program;
+ }
+ if (i == programs.size() - 1) {
+ return program;
+ } else {
+ return programs.get(i + 1);
+ }
+ }
+ return programs.isEmpty() ? null : programs.get(0);
+ }
+
@Override
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
@@ -166,43 +257,57 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
for (SeriesRecording series : seriesRecordings) {
if (mSeries.getId() == series.getId()) {
mSeries = series;
- // TODO: change action label.
}
}
}
@Override
- public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { }
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (series.getId() == mSeries.getId()) {
+ mSeries = null;
+ getActivity().finish();
+ return;
+ }
+ }
+ }
@Override
- public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
- if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
- addProgram(recordedProgram);
- if (mActionsAdapter.lookup(ACTION_DELETE) == null) {
- mActionsAdapter.set(ACTION_DELETE, mDeleteAction);
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
+ addProgram(recordedProgram);
+ if (mActionsAdapter.lookup(ACTION_DELETE) == null) {
+ mActionsAdapter.set(ACTION_DELETE, mDeleteAction);
+ }
}
}
}
@Override
- public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
// Do nothing
}
@Override
- public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
- if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
- ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false);
- if (row != null) {
- SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter();
- adapter.remove(recordedProgram);
- if (adapter.isEmpty()) {
- getRowsAdapter().remove(row);
- if (getRowsAdapter().size() == 1) {
- // No season rows left. Only DetailsOverviewRow
- mActionsAdapter.clear(ACTION_DELETE);
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
+ ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false);
+ if (row != null) {
+ SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter();
+ adapter.remove(recordedProgram);
+ if (adapter.isEmpty()) {
+ getRowsAdapter().remove(row);
+ if (getRowsAdapter().size() == 1) {
+ // No season rows left. Only DetailsOverviewRow
+ mActionsAdapter.clear(ACTION_DELETE);
+ }
}
}
+ if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
+ updateWatchAction();
+ }
}
}
}
@@ -224,7 +329,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
for (int i = rowsAdaptor.size() - 1; i >= 0; i--) {
Object row = rowsAdaptor.get(i);
if (row instanceof ListRow) {
- int compareResult = RecordedProgram.numberCompare(seasonNumber,
+ int compareResult = BaseProgram.numberCompare(seasonNumber,
((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber);
if (compareResult == 0) {
return (ListRow) row;
@@ -241,8 +346,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
: getString(R.string.dvr_detail_series_season_title, seasonNumber);
HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle);
ClassPresenterSelector selector = new ClassPresenterSelector();
- selector.addClassPresenter(RecordedProgram.class,
- new RecordedProgramPresenter(getContext(), true));
+ selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter);
ListRow row = new ListRow(header, new SeasonRowAdapter(selector,
new Comparator<RecordedProgram>() {
@Override
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
index d2f26dd1..c2c0f596 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
@@ -18,24 +18,20 @@ package com.android.tv.dvr.ui;
import android.app.Activity;
import android.content.Context;
-import android.content.Intent;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
-import android.os.Bundle;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v4.app.ActivityOptionsCompat;
import android.text.TextUtils;
-import android.view.View;
import android.view.ViewGroup;
-import com.android.tv.R;
import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
import com.android.tv.dvr.RecordedProgram;
@@ -45,11 +41,12 @@ import com.android.tv.dvr.SeriesRecording;
import java.util.List;
/**
- * Presents a {@link SeriesRecording} in the {@link DvrBrowseFragment}.
+ * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}.
*/
-public class SeriesRecordingPresenter extends Presenter {
+public class SeriesRecordingPresenter extends DvrItemPresenter {
private final ChannelDataManager mChannelDataManager;
private final DvrDataManager mDvrDataManager;
+ private final DvrManager mDvrManager;
private final DvrWatchedPositionManager mWatchedPositionManager;
private static final class SeriesRecordingViewHolder extends ViewHolder implements
@@ -57,13 +54,15 @@ public class SeriesRecordingPresenter extends Presenter {
private SeriesRecording mSeriesRecording;
private RecordingCardView mCardView;
private DvrDataManager mDvrDataManager;
+ private DvrManager mDvrManager;
private DvrWatchedPositionManager mWatchedPositionManager;
SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager,
- DvrWatchedPositionManager watchedPositionManager) {
+ DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) {
super(view);
mCardView = view;
mDvrDataManager = dvrDataManager;
+ mDvrManager = dvrManager;
mWatchedPositionManager = watchedPositionManager;
}
@@ -96,27 +95,41 @@ public class SeriesRecordingPresenter extends Presenter {
}
@Override
- public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
- if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) {
- mDvrDataManager.removeScheduledRecordingListener(this);
- mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ boolean needToUpdateCardView = false;
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(),
+ mSeriesRecording.getSeriesId())) {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ needToUpdateCardView = true;
+ }
+ }
+ if (needToUpdateCardView) {
updateCardViewContent();
}
}
@Override
- public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
- if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) {
- if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
- == TvInputManager.TIME_SHIFT_INVALID_TIME) {
- mWatchedPositionManager.removeListener(this, recordedProgram.getId());
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ boolean needToUpdateCardView = false;
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(),
+ mSeriesRecording.getSeriesId())) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.removeListener(this, recordedProgram.getId());
+ }
+ needToUpdateCardView = true;
}
+ }
+ if (needToUpdateCardView) {
updateCardViewContent();
}
}
@Override
- public void onRecordedProgramChanged(RecordedProgram recordedProgram) {
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
// Do nothing
}
@@ -151,7 +164,7 @@ public class SeriesRecordingPresenter extends Presenter {
List<RecordedProgram> recordedPrograms =
mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId());
if (recordedPrograms.size() == 0) {
- count = mDvrDataManager.getScheduledRecordings(mSeriesRecording.getId()).size();
+ count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size();
quantityStringID = R.plurals.dvr_count_scheduled_recordings;
} else {
for (RecordedProgram recordedProgram : recordedPrograms) {
@@ -176,6 +189,7 @@ public class SeriesRecordingPresenter extends Presenter {
ApplicationSingletons singletons = TvApplication.getSingletons(context);
mChannelDataManager = singletons.getChannelDataManager();
mDvrDataManager = singletons.getDvrDataManager();
+ mDvrManager = singletons.getDvrManager();
mWatchedPositionManager = singletons.getDvrWatchedPositionManager();
}
@@ -183,7 +197,8 @@ public class SeriesRecordingPresenter extends Presenter {
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Context context = parent.getContext();
RecordingCardView view = new RecordingCardView(context);
- return new SeriesRecordingViewHolder(view, mDvrDataManager, mWatchedPositionManager);
+ return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager,
+ mWatchedPositionManager);
}
@Override
@@ -193,33 +208,14 @@ public class SeriesRecordingPresenter extends Presenter {
final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
viewHolder.onBound(seriesRecording);
setTitleAndImage(cardView, seriesRecording);
- View.OnClickListener clickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- showSeriesRecordingDetails(v, seriesRecording);
- }
- };
- baseHolder.view.setOnClickListener(clickListener);
- }
-
- private void showSeriesRecordingDetails(View view, SeriesRecording seriesRecording) {
- if (view instanceof RecordingCardView) {
- Context context = view.getContext();
- Intent intent = new Intent(context, DvrDetailsActivity.class);
- intent.putExtra(DvrDetailsActivity.RECORDING_ID, seriesRecording.getId());
- intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE,
- DvrDetailsActivity.SERIES_RECORDING_VIEW);
- Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context,
- ((RecordingCardView) view).getImageView(),
- DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle();
- context.startActivity(intent, bundle);
- }
+ super.onBindViewHolder(baseHolder, o);
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
((RecordingCardView) viewHolder.view).reset();
((SeriesRecordingViewHolder) viewHolder).onUnbound();
+ super.onUnbindViewHolder(viewHolder);
}
private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) {
diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
index c550935c..6c05c9c6 100644
--- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
@@ -17,45 +17,64 @@
package com.android.tv.dvr.ui;
import android.app.FragmentManager;
+import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.EpisodicProgramLoadTask;
import com.android.tv.dvr.SeriesRecording;
import com.android.tv.dvr.SeriesRecording.ChannelOption;
-
+import com.android.tv.dvr.SeriesRecordingScheduler;
+import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener;
import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Fragment for DVR series recording settings.
*/
public class SeriesSettingsFragment extends GuidedStepFragment
implements DvrDataManager.SeriesRecordingListener {
- /**
- * Name of series recording id added to the bundle.
- * Type: Long
- */
- public static final String SERIES_RECORDING_ID = "series_recording_id";
+ private static final String TAG = "SeriesSettingsFragment";
+ private static final boolean DEBUG = false;
private static final long ACTION_ID_PRIORITY = 10;
private static final long ACTION_ID_CHANNEL = 11;
- private static final long SUB_ACTION_ID_CHANNEL_ONE = 101;
private static final long SUB_ACTION_ID_CHANNEL_ALL = 102;
+ // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id
+ private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500;
private DvrDataManager mDvrDataManager;
+ private ChannelDataManager mChannelDataManager;
+ private DvrManager mDvrManager;
private SeriesRecording mSeriesRecording;
- private Channel mChannel;
private long mSeriesRecordingId;
@ChannelOption int mChannelOption;
+ private Comparator<Channel> mChannelComparator;
+ private long mSelectedChannelId;
+ private int mBackStackCount;
+ private boolean mShowViewScheduleOptionInDialog;
private String mFragmentTitle;
private String mProrityActionTitle;
@@ -63,6 +82,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private String mProrityActionLowestText;
private String mChannelsActionTitle;
private String mChannelsActionAllText;
+ private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>();
+ private List<Channel> mChannels = new ArrayList<>();
+ private EpisodicProgramLoadTask mEpisodicProgramLoadTask;
private GuidedAction mPriorityGuidedAction;
private GuidedAction mChannelsGuidedAction;
@@ -70,14 +92,49 @@ public class SeriesSettingsFragment extends GuidedStepFragment
@Override
public void onAttach(Context context) {
super.onAttach(context);
+ mBackStackCount = getFragmentManager().getBackStackEntryCount();
mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
- mSeriesRecordingId = getArguments().getLong(SERIES_RECORDING_ID);
+ mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID);
mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
+ if (mSeriesRecording == null) {
+ getActivity().finish();
+ return;
+ }
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mShowViewScheduleOptionInDialog = getArguments().getBoolean(
+ DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG);
mDvrDataManager.addSeriesRecordingListener(this);
+ long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST);
+ mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
+ if (channelIds == null) {
+ Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
+ if (channel != null) {
+ mId2Channel.put(channel.getId(), channel);
+ mChannels.add(channel);
+ }
+ collectChannelsInBackground();
+ } else {
+ for (long channelId : channelIds) {
+ Channel channel = mChannelDataManager.getChannel(channelId);
+ if (channel != null) {
+ mId2Channel.put(channel.getId(), channel);
+ mChannels.add(channel);
+ }
+ }
+ }
mChannelOption = mSeriesRecording.getChannelOption();
- mChannel = TvApplication.getSingletons(context).getChannelDataManager()
- .getChannel(mSeriesRecording.getChannelId());
- // TODO: Handle when channel is null.
+ mSelectedChannelId = Channel.INVALID_ID;
+ if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) {
+ Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
+ if (channel != null) {
+ mSelectedChannelId = channel.getId();
+ } else {
+ mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL;
+ }
+ }
+ mChannelComparator = new Channel.DefaultComparator(context,
+ TvApplication.getSingletons(context).getTvInputManagerHelper());
+ mChannels.sort(mChannelComparator);
mFragmentTitle = getString(R.string.dvr_series_settings_title);
mProrityActionTitle = getString(R.string.dvr_series_settings_priority);
mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest);
@@ -90,6 +147,22 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onDetach() {
super.onDetach();
mDvrDataManager.removeSeriesRecordingListener(this);
+ if (mEpisodicProgramLoadTask != null) {
+ mEpisodicProgramLoadTask.cancel(true);
+ mEpisodicProgramLoadTask = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
+ if (getFragmentManager().getBackStackEntryCount() == mBackStackCount
+ && getArguments()
+ .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)
+ && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) {
+ dvrManager.removeSeriesRecording(mSeriesRecordingId);
+ }
+ super.onDestroy();
}
@Override
@@ -108,19 +181,10 @@ public class SeriesSettingsFragment extends GuidedStepFragment
updatePriorityGuidedAction(false);
actions.add(mPriorityGuidedAction);
- List<GuidedAction> channelSubActions = new ArrayList<GuidedAction>();
- channelSubActions.add(new GuidedAction.Builder(getActivity())
- .id(SUB_ACTION_ID_CHANNEL_ONE)
- .title(mChannel.getDisplayText())
- .build());
- channelSubActions.add(new GuidedAction.Builder(getActivity())
- .id(SUB_ACTION_ID_CHANNEL_ALL)
- .title(mChannelsActionAllText)
- .build());
mChannelsGuidedAction = new GuidedAction.Builder(getActivity())
.id(ACTION_ID_CHANNEL)
.title(mChannelsActionTitle)
- .subActions(channelSubActions)
+ .subActions(buildChannelSubAction())
.build();
actions.add(mChannelsGuidedAction);
updateChannelsGuidedAction(false);
@@ -140,13 +204,45 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == GuidedAction.ACTION_ID_OK) {
- if (mChannelOption != mSeriesRecording.getChannelOption()) {
+ if (mEpisodicProgramLoadTask != null) {
+ mEpisodicProgramLoadTask.cancel(true);
+ mEpisodicProgramLoadTask = null;
+ }
+ if (mChannelOption != mSeriesRecording.getChannelOption()
+ || mSeriesRecording.isStopped()
+ || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE
+ && mSeriesRecording.getChannelId() != mSelectedChannelId)) {
+ SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording)
+ .setChannelOption(mChannelOption)
+ .setState(SeriesRecording.STATE_SERIES_NORMAL);
+ if (mSelectedChannelId != Channel.INVALID_ID) {
+ builder.setChannelId(mSelectedChannelId);
+ }
TvApplication.getSingletons(getContext()).getDvrManager()
- .updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording)
- .setChannelOption(mChannelOption)
- .build());
+ .updateSeriesRecording(builder.build());
+ SeriesRecordingScheduler scheduler =
+ SeriesRecordingScheduler.getInstance(getContext());
+ // Since dialog is used even after the fragment is closed, we should
+ // use application context.
+ ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString(
+ R.string.dvr_series_schedules_progress_message_updating_programs));
+ scheduler.addOnSeriesRecordingUpdatedListener(
+ new OnSeriesRecordingUpdatedListener() {
+ @Override
+ public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ if (seriesRecording.getId() == mSeriesRecordingId) {
+ dialog.dismiss();
+ scheduler.removeOnSeriesRecordingUpdatedListener(this);
+ showConfirmDialog();
+ return;
+ }
+ }
+ }
+ });
+ } else {
+ showConfirmDialog();
}
- finishGuidedStepFragments();
} else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
finishGuidedStepFragments();
} else if (actionId == ACTION_ID_PRIORITY) {
@@ -165,10 +261,12 @@ public class SeriesSettingsFragment extends GuidedStepFragment
long actionId = action.getId();
if (actionId == SUB_ACTION_ID_CHANNEL_ALL) {
mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL;
+ mSelectedChannelId = Channel.INVALID_ID;
updateChannelsGuidedAction(true);
return true;
- } else if (actionId == SUB_ACTION_ID_CHANNEL_ONE) {
+ } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) {
mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE;
+ mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE;
updateChannelsGuidedAction(true);
return true;
}
@@ -184,7 +282,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment
if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) {
mChannelsGuidedAction.setDescription(mChannelsActionAllText);
} else {
- mChannelsGuidedAction.setDescription(mChannel.getDisplayText());
+ mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId)
+ .getDisplayText());
}
if (notifyActionChanged) {
notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL));
@@ -210,13 +309,75 @@ public class SeriesSettingsFragment extends GuidedStepFragment
} else if (priorityOrder >= totalSeriesCount - 1) {
mPriorityGuidedAction.setDescription(mProrityActionLowestText);
} else {
- mPriorityGuidedAction.setDescription(Integer.toString(priorityOrder + 1));
+ mPriorityGuidedAction.setDescription(getString(
+ R.string.dvr_series_settings_priority_rank, priorityOrder + 1));
}
if (notifyActionChanged) {
notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
}
}
+ private void collectChannelsInBackground() {
+ if (mEpisodicProgramLoadTask != null) {
+ mEpisodicProgramLoadTask.cancel(true);
+ }
+ mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ mEpisodicProgramLoadTask = null;
+ Set<Long> channelIds = new HashSet<>();
+ for (Program program : programs) {
+ channelIds.add(program.getChannelId());
+ }
+ boolean channelAdded = false;
+ for (Long channelId : channelIds) {
+ if (mId2Channel.get(channelId) != null) {
+ continue;
+ }
+ Channel channel = mChannelDataManager.getChannel(channelId);
+ if (channel != null) {
+ channelAdded = true;
+ mId2Channel.put(channelId, channel);
+ mChannels.add(channel);
+ if (DEBUG) Log.d(TAG, "Added channel: " + channel);
+ }
+ }
+ if (!channelAdded) {
+ return;
+ }
+ mChannels.sort(mChannelComparator);
+ mChannelsGuidedAction.setSubActions(buildChannelSubAction());
+ notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL));
+ if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask");
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true);
+ mEpisodicProgramLoadTask.execute();
+ }
+
+ private List<GuidedAction> buildChannelSubAction() {
+ List<GuidedAction> channelSubActions = new ArrayList<>();
+ channelSubActions.add(new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_CHANNEL_ALL)
+ .title(mChannelsActionAllText)
+ .build());
+ for (Channel channel : mChannels) {
+ channelSubActions.add(new GuidedAction.Builder(getActivity())
+ .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId())
+ .title(channel.getDisplayText())
+ .build());
+ }
+ return channelSubActions;
+ }
+
+ private void showConfirmDialog() {
+ DvrUiHelper.StartSeriesScheduledDialogActivity(
+ getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog);
+ finishGuidedStepFragments();
+ }
+
@Override
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
index 3a57d72e..393a5ff3 100644
--- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -148,7 +148,10 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
return -1;
}
- private int findInsertPosition(T item) {
+ /**
+ * Finds the position that the given item should be inserted to keep the sorted order.
+ */
+ public int findInsertPosition(T item) {
for (int i = size() - mExtraItemCount - 1; i >=0; i--) {
T r = (T) get(i);
if (mComparator.compare(r, item) <= 0) {
diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
index 61de5764..d28f026c 100644
--- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
@@ -22,12 +22,13 @@ import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.widget.TextView;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.ScheduledRecording;
/**
@@ -35,32 +36,39 @@ import com.android.tv.dvr.ScheduledRecording;
*/
public abstract class BaseDvrSchedulesFragment extends DetailsFragment
implements DvrDataManager.ScheduledRecordingListener,
- SchedulesHeaderRowPresenter.SchedulesHeaderRowListener,
- ScheduleRowPresenter.ScheduleRowClickListener {
+ DvrScheduleManager.OnConflictStateChangeListener {
/**
* The key for scheduled recording which has be selected in the list.
*/
public static String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording";
- private SchedulesHeaderRowPresenter mHeaderRowPresenter;
- private ScheduleRowPresenter mRowPresenter;
private ScheduleRowAdapter mRowsAdapter;
+ private TextView mEmptyInfoScreenView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
- mHeaderRowPresenter = onCreateHeaderRowPresenter();
- mHeaderRowPresenter.addListener(this);
- mRowPresenter = onCreateRowPresenter();
- mRowPresenter.addListener(this);
- presenterSelector.addClassPresenter(SchedulesHeaderRow.class, mHeaderRowPresenter);
- presenterSelector.addClassPresenter(ScheduleRow.class, mRowPresenter);
+ presenterSelector.addClassPresenter(SchedulesHeaderRow.class, onCreateHeaderRowPresenter());
+ presenterSelector.addClassPresenter(ScheduleRow.class, onCreateRowPresenter());
mRowsAdapter = onCreateRowsAdapter(presenterSelector);
setAdapter(mRowsAdapter);
mRowsAdapter.start();
- TvApplication.getSingletons(getContext()).getDvrDataManager()
- .addScheduledRecordingListener(this);
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ singletons.getDvrDataManager().addScheduledRecordingListener(this);
+ singletons.getDvrScheduleManager().addOnConflictStateChangeListener(this);
+ mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = super.onCreateView(inflater, container, savedInstanceState);
+ int firstItemPosition = getFirstItemPosition();
+ if (firstItemPosition != -1) {
+ getRowsFragment().setSelectedPosition(firstItemPosition, false);
+ }
+ return view;
}
/**
@@ -73,34 +81,20 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
/**
* Shows the empty message.
*/
- protected void showEmptyMessage(int message) {
- TextView emptyInfoScreenView = (TextView) getActivity().findViewById(
- R.id.empty_info_screen);
- emptyInfoScreenView.setText(message);
- emptyInfoScreenView.setVisibility(View.VISIBLE);
+ void showEmptyMessage(int messageId) {
+ mEmptyInfoScreenView.setText(messageId);
+ if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) {
+ mEmptyInfoScreenView.setVisibility(View.VISIBLE);
+ }
}
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View view = super.onCreateView(inflater, container, savedInstanceState);
- // setSelectedPosition works only after the view is attached to a window.
- view.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- int firstItemPosition = getFirstItemPosition();
- if (firstItemPosition != -1) {
- setSelectedPosition(firstItemPosition, false);
- }
- view.getViewTreeObserver().removeOnWindowAttachListener(this);
- }
-
- @Override
- public void onWindowDetached() {
- }
- });
- return view;
+ /**
+ * Hides the empty message.
+ */
+ void hideEmptyMessage() {
+ if (mEmptyInfoScreenView.getVisibility() == View.VISIBLE) {
+ mEmptyInfoScreenView.setVisibility(View.GONE);
+ }
}
@Override
@@ -112,10 +106,9 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
@Override
public void onDestroy() {
- TvApplication.getSingletons(getContext()).getDvrDataManager()
- .removeScheduledRecordingListener(this);
- mHeaderRowPresenter.removeListener(this);
- mRowPresenter.removeListener(this);
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ singletons.getDvrScheduleManager().removeOnConflictStateChangeListener(this);
+ singletons.getDvrDataManager().removeScheduledRecordingListener(this);
mRowsAdapter.stop();
super.onDestroy();
}
@@ -139,16 +132,6 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
* Gets the first focus position in schedules list.
*/
protected int getFirstItemPosition() {
- Bundle args = getArguments();
- ScheduledRecording recording = null;
- if (args != null) {
- recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING);
- }
- final int selectedPostion = mRowsAdapter.indexOf(
- mRowsAdapter.findRowByScheduledRecording(recording));
- if (selectedPostion != -1) {
- return selectedPostion;
- }
for (int i = 0; i < mRowsAdapter.size(); i++) {
if (mRowsAdapter.get(i) instanceof ScheduleRow) {
return i;
@@ -159,11 +142,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
@Override
public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
- for (ScheduledRecording recording : scheduledRecordings) {
- if (mRowPresenter != null) {
- mRowPresenter.onScheduledRecordingAdded(recording);
- }
- if (mRowsAdapter != null) {
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
mRowsAdapter.onScheduledRecordingAdded(recording);
}
}
@@ -171,11 +151,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
@Override
public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
- for (ScheduledRecording recording : scheduledRecordings) {
- if (mRowPresenter != null) {
- mRowPresenter.onScheduledRecordingRemoved(recording);
- }
- if (mRowsAdapter != null) {
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
mRowsAdapter.onScheduledRecordingRemoved(recording);
}
}
@@ -183,27 +160,19 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
@Override
public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
- for (ScheduledRecording recording : scheduledRecordings) {
- if (mRowPresenter != null) {
- mRowPresenter.onScheduledRecordingUpdated(recording);
- }
- if (mRowsAdapter != null) {
- mRowsAdapter.onScheduledRecordingUpdated(recording);
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ mRowsAdapter.onScheduledRecordingUpdated(recording, false);
}
}
}
@Override
- public void onUpdateAllScheduleRows() {
- if (getRowsAdapter() != null) {
- getRowsAdapter().notifyArrayItemRangeChanged(0, getRowsAdapter().size());
- }
- }
-
- @Override
- public void onDeleteClicked(ScheduleRow scheduleRow) {
+ public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) {
if (mRowsAdapter != null) {
- mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());
+ for (ScheduledRecording recording : schedules) {
+ mRowsAdapter.onScheduledRecordingUpdated(recording, true);
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
index f361ede3..722c9b6e 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
@@ -18,8 +18,12 @@ package com.android.tv.dvr.ui.list;
import android.os.Bundle;
import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
import com.android.tv.R;
+import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
/**
@@ -48,4 +52,35 @@ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment {
public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor) {
return new ScheduleRowAdapter(getContext(), presenterSelecor);
}
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ super.onScheduledRecordingAdded(scheduledRecordings);
+ if (getRowsAdapter().size() > 0) {
+ hideEmptyMessage();
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ super.onScheduledRecordingRemoved(scheduledRecordings);
+ if (getRowsAdapter().size() == 0) {
+ showEmptyMessage(R.string.dvr_schedules_empty_state);
+ }
+ }
+
+ @Override
+ protected int getFirstItemPosition() {
+ Bundle args = getArguments();
+ ScheduledRecording recording = null;
+ if (args != null) {
+ recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING);
+ }
+ final int selectedPostion = getRowsAdapter().indexOf(
+ getRowsAdapter().findRowByScheduledRecording(recording));
+ if (selectedPostion != -1) {
+ return selectedPostion;
+ }
+ return super.getFirstItemPosition();
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
index ba8b0c36..42a1e72b 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -16,71 +16,154 @@
package com.android.tv.dvr.ui.list;
+import android.annotation.TargetApi;
+import android.database.ContentObserver;
+import android.media.tv.TvContract.Programs;
+import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.transition.Fade;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
+import com.android.tv.dvr.EpisodicProgramLoadTask;
import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.ui.DvrSchedulesActivity;
-import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter;
+import java.util.List;
+
/**
* A fragment to show the list of series schedule recordings.
*/
+@TargetApi(Build.VERSION_CODES.N)
public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
+ private static final String TAG = "DvrSeriesSchedulesFragment";
/**
* The key for series recording whose scheduled recording list will be displayed.
*/
- public static String SERIES_SCHEDULES_KEY_SERIES_RECORDING =
+ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING =
"series_schedules_key_series_recording";
+ /**
+ * The key for programs belong to the series recording whose scheduled recording
+ * list will be displayed.
+ */
+ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS =
+ "series_schedules_key_series_programs";
+
+ private ChannelDataManager mChannelDataManager;
+ private SeriesRecording mSeriesRecording;
+ private List<Program> mPrograms;
+ private EpisodicProgramLoadTask mProgramLoadTask;
+
+ private final SeriesRecordingListener mSeriesRecordingListener =
+ new SeriesRecordingListener() {
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording r : seriesRecordings) {
+ if (r.getId() == mSeriesRecording.getId()) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording r : seriesRecordings) {
+ if (r.getId() == mSeriesRecording.getId()
+ && getRowsAdapter() instanceof SeriesScheduleRowAdapter) {
+ ((SeriesScheduleRowAdapter) getRowsAdapter())
+ .onSeriesRecordingUpdated(r);
+ return;
+ }
+ }
+ }
+ };
- private static String TAG = "DvrSeriesSchedulesFragment";
+ private final ContentObserver mContentObserver =
+ new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ executeProgramLoadingTask();
+ }
+ };
+
+ private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() { }
- private SeriesRecording mSeries;
+ @Override
+ public void onChannelListUpdated() {
+ executeProgramLoadingTask();
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
+
+ public DvrSeriesSchedulesFragment() {
+ setEnterTransition(new Fade(Fade.IN));
+ }
@Override
public void onCreate(Bundle savedInstanceState) {
Bundle args = getArguments();
if (args != null) {
- mSeries = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS);
}
super.onCreate(savedInstanceState);
- // "1" means there is only title row in series schedules list. So we should show an empty
- // state info view.
- if (getRowsAdapter().size() == 1) {
- showEmptyMessage(R.string.dvr_series_schedules_empty_state);
- }
- ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(new Runnable() {
- @Override
- public void run() {
- SoftPreconditions.checkState(getRowsAdapter().get(0) instanceof
- SeriesRecordingHeaderRow, TAG, "First row is not SchedulesHeaderRow");
- SeriesRecordingHeaderRow headerRow =
- (SeriesRecordingHeaderRow) getRowsAdapter().get(0);
- headerRow.setCancelAllChecked(true);
- if (headerRow.getSeriesRecording() != null) {
- TvApplication.getSingletons(getContext()).getDvrManager()
- .updateSeriesRecording(SeriesRecording.buildFrom(
- headerRow.getSeriesRecording()).setState(
- SeriesRecording.STATE_SERIES_CANCELED).build());
- }
- onUpdateAllScheduleRows();
- }
- });
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener);
+ mChannelDataManager = singletons.getChannelDataManager();
+ mChannelDataManager.addListener(mChannelListener);
+ getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
+ mContentObserver);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
+ onProgramsUpdated();
return super.onCreateView(inflater, container, savedInstanceState);
}
+ private void onProgramsUpdated() {
+ ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms);
+ if (mPrograms == null || mPrograms.isEmpty()) {
+ showEmptyMessage(R.string.dvr_series_schedules_empty_state);
+ } else {
+ hideEmptyMessage();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProgramLoadTask != null) {
+ mProgramLoadTask.cancel(true);
+ mProgramLoadTask = null;
+ }
+ getContext().getContentResolver().unregisterContentObserver(mContentObserver);
+ mChannelDataManager.removeListener(mChannelListener);
+ TvApplication.getSingletons(getContext()).getDvrDataManager()
+ .removeSeriesRecordingListener(mSeriesRecordingListener);
+ super.onDestroy();
+ }
+
@Override
public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() {
return new SeriesRecordingHeaderRowPresenter(getContext());
@@ -93,20 +176,33 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
@Override
public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelector) {
- return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeries);
+ return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeriesRecording);
}
@Override
protected int getFirstItemPosition() {
- if (mSeries != null && mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED) {
- return -1;
+ if (mSeriesRecording != null
+ && mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) {
+ return 0;
}
return super.getFirstItemPosition();
}
- @Override
- public void onDestroy() {
- ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(null);
- super.onDestroy();
+ private void executeProgramLoadingTask() {
+ if (mProgramLoadTask != null) {
+ mProgramLoadTask.cancel(true);
+ }
+ mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ mPrograms = programs;
+ onProgramsUpdated();
+ }
+ };
+ mProgramLoadTask.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true)
+ .execute();
}
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
new file mode 100644
index 00000000..23aebf59
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui.list;
+
+import android.content.Context;
+
+import com.android.tv.data.Program;
+import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.ScheduledRecording.Builder;
+
+/**
+ * A class for the episodic program.
+ */
+public class EpisodicProgramRow extends ScheduleRow {
+ private final String mInputId;
+ private final Program mProgram;
+
+ public EpisodicProgramRow(String inputId, Program program, ScheduledRecording recording,
+ SchedulesHeaderRow headerRow) {
+ super(recording, headerRow);
+ mInputId = inputId;
+ mProgram = program;
+ }
+
+ /**
+ * Returns the program.
+ */
+ public Program getProgram() {
+ return mProgram;
+ }
+
+ @Override
+ public long getChannelId() {
+ return mProgram.getChannelId();
+ }
+
+ @Override
+ public long getStartTimeMs() {
+ return mProgram.getStartTimeUtcMillis();
+ }
+
+ @Override
+ public long getEndTimeMs() {
+ return mProgram.getEndTimeUtcMillis();
+ }
+
+ @Override
+ public Builder createNewScheduleBuilder() {
+ return ScheduledRecording.builder(mInputId, mProgram);
+ }
+
+ @Override
+ public String getProgramTitleWithEpisodeNumber(Context context) {
+ return mProgram.getTitleWithEpisodeNumber(context);
+ }
+
+ @Override
+ public String getEpisodeDisplayTitle(Context context) {
+ return mProgram.getEpisodeDisplayTitle(context);
+ }
+
+ @Override
+ public boolean matchSchedule(ScheduledRecording schedule) {
+ return schedule.getType() == ScheduledRecording.TYPE_PROGRAM
+ && mProgram.getId() == schedule.getProgramId();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString()
+ + "(inputId=" + mInputId
+ + ",program=" + mProgram
+ + ")";
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
index 1e258d2d..3fc92e8a 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
@@ -16,54 +16,188 @@
package com.android.tv.dvr.ui.list;
+import android.content.Context;
+import android.support.annotation.Nullable;
+
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.ScheduledRecording;
/**
* A class for schedule recording row.
*/
public class ScheduleRow {
- private ScheduledRecording mRecording;
- private boolean mRemoveScheduleChecked;
- private SchedulesHeaderRow mHeaderRow;
+ private final SchedulesHeaderRow mHeaderRow;
+ @Nullable private ScheduledRecording mSchedule;
+ private boolean mStopRecordingRequested;
+ private boolean mStartRecordingRequested;
- public ScheduleRow(ScheduledRecording recording, SchedulesHeaderRow headerRow) {
- mRecording = recording;
- mRemoveScheduleChecked = false;
+ public ScheduleRow(@Nullable ScheduledRecording recording, SchedulesHeaderRow headerRow) {
+ mSchedule = recording;
mHeaderRow = headerRow;
}
/**
- * Sets scheduled recording.
+ * Gets which {@link SchedulesHeaderRow} this schedule row belongs to.
*/
- public void setRecording(ScheduledRecording recording) {
- mRecording = recording;
+ public SchedulesHeaderRow getHeaderRow() {
+ return mHeaderRow;
}
/**
- * Sets remove schedule checked status.
+ * Returns the recording schedule.
*/
- public void setRemoveScheduleChecked(boolean checked) {
- mRemoveScheduleChecked = checked;
+ @Nullable
+ public ScheduledRecording getSchedule() {
+ return mSchedule;
}
/**
- * Gets scheduled recording.
+ * Checks if the stop recording has been requested or not.
*/
- public ScheduledRecording getRecording() {
- return mRecording;
+ public boolean isStopRecordingRequested() {
+ return mStopRecordingRequested;
}
/**
- * Gets remove schedule checked status.
+ * Sets the flag of stop recording request.
*/
- public boolean isRemoveScheduleChecked() {
- return mRemoveScheduleChecked;
+ public void setStopRecordingRequested(boolean stopRecordingRequested) {
+ SoftPreconditions.checkState(!mStartRecordingRequested);
+ mStopRecordingRequested = stopRecordingRequested;
}
/**
- * Gets which {@link SchedulesHeaderRow} this schedule row belongs to.
+ * Checks if the start recording has been requested or not.
*/
- public SchedulesHeaderRow getHeaderRow() {
- return mHeaderRow;
+ public boolean isStartRecordingRequested() {
+ return mStartRecordingRequested;
+ }
+
+ /**
+ * Sets the flag of start recording request.
+ */
+ public void setStartRecordingRequested(boolean startRecordingRequested) {
+ SoftPreconditions.checkState(!mStopRecordingRequested);
+ mStartRecordingRequested = startRecordingRequested;
+ }
+
+ /**
+ * Sets the recording schedule.
+ */
+ public void setSchedule(@Nullable ScheduledRecording schedule) {
+ mSchedule = schedule;
+ }
+
+ /**
+ * Returns the channel ID.
+ */
+ public long getChannelId() {
+ return mSchedule != null ? mSchedule.getChannelId() : -1;
+ }
+
+ /**
+ * Returns the start time.
+ */
+ public long getStartTimeMs() {
+ return mSchedule != null ? mSchedule.getStartTimeMs() : -1;
+ }
+
+ /**
+ * Returns the end time.
+ */
+ public long getEndTimeMs() {
+ return mSchedule != null ? mSchedule.getEndTimeMs() : -1;
+ }
+
+ /**
+ * Returns the duration.
+ */
+ public final long getDuration() {
+ return getEndTimeMs() - getStartTimeMs();
+ }
+
+ /**
+ * Checks if the program is on air.
+ */
+ public final boolean isOnAir() {
+ long currentTimeMs = System.currentTimeMillis();
+ return getStartTimeMs() <= currentTimeMs && getEndTimeMs() > currentTimeMs;
+ }
+
+ /**
+ * Checks if the schedule is not started.
+ */
+ public final boolean isRecordingNotStarted() {
+ return mSchedule != null
+ && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
+ }
+
+ /**
+ * Checks if the schedule is in progress.
+ */
+ public final boolean isRecordingInProgress() {
+ return mSchedule != null
+ && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS;
+ }
+
+ /**
+ * Checks if the schedule has been canceled or not.
+ */
+ public final boolean isScheduleCanceled() {
+ return mSchedule != null
+ && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED;
+ }
+
+ public boolean isRecordingFinished() {
+ return mSchedule != null
+ && (mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED
+ || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
+ || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED);
+ }
+
+ /**
+ * Creates and returns the new schedule with the existing information.
+ */
+ public ScheduledRecording.Builder createNewScheduleBuilder() {
+ return mSchedule != null ? ScheduledRecording.buildFrom(mSchedule) : null;
+ }
+
+ /**
+ * Returns the program title with episode number.
+ */
+ public String getProgramTitleWithEpisodeNumber(Context context) {
+ return mSchedule != null ? mSchedule.getProgramTitleWithEpisodeNumber(context) : null;
+ }
+
+ /**
+ * Returns the program title including the season/episode number.
+ */
+ public String getEpisodeDisplayTitle(Context context) {
+ return mSchedule != null ? mSchedule.getEpisodeDisplayTitle(context) : null;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "(schedule=" + mSchedule
+ + ",stopRecordingRequested=" + mStopRecordingRequested
+ + ",startRecordingRequested=" + mStartRecordingRequested
+ + ")";
+ }
+
+ /**
+ * Checks if the {@code schedule} is for the program or channel.
+ */
+ public boolean matchSchedule(ScheduledRecording schedule) {
+ if (mSchedule == null) {
+ return false;
+ }
+ if (mSchedule.getType() == ScheduledRecording.TYPE_TIMED) {
+ return mSchedule.getChannelId() == schedule.getChannelId()
+ && mSchedule.getStartTimeMs() == schedule.getStartTimeMs()
+ && mSchedule.getEndTimeMs() == schedule.getEndTimeMs();
+ } else {
+ return mSchedule.getProgramId() == schedule.getProgramId();
+ }
}
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index 3e2630c7..9cc82653 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -17,12 +17,19 @@
package com.android.tv.dvr.ui.list;
import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
import com.android.tv.util.Utils;
@@ -30,16 +37,34 @@ import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* An adapter for {@link ScheduleRow}.
*/
public class ScheduleRowAdapter extends ArrayObjectAdapter {
+ private static final String TAG = "ScheduleRowAdapter";
+ private static final boolean DEBUG = false;
+
private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
+ private static final int MSG_UPDATE_ROW = 1;
+
private Context mContext;
private final List<String> mTitles = new ArrayList<>();
+ private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>();
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_UPDATE_ROW) {
+ long currentTimeMs = System.currentTimeMillis();
+ handleUpdateRow(currentTimeMs);
+ sendNextUpdateMessage(currentTimeMs);
+ }
+ }
+ };
public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) {
super(classPresenterSelector);
@@ -64,7 +89,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
.getDvrDataManager().getNonStartedScheduledRecordings();
recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager()
.getStartedRecordings());
- Collections.sort(recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
+ Collections.sort(recordingList,
+ ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis());
for (int i = 0; i < recordingList.size();) {
ArrayList<ScheduledRecording> section = new ArrayList<>();
@@ -83,6 +109,7 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
}
deadLine += ONE_DAY_MS;
}
+ sendNextUpdateMessage(System.currentTimeMillis());
}
private String calculateHeaderDate(long deadLine) {
@@ -93,8 +120,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
headerDate = mTitles.get(titleIndex);
} else {
headerDate = DateUtils.formatDateTime(getContext(), deadLine,
- DateUtils.FORMAT_SHOW_WEEKDAY| DateUtils.FORMAT_ABBREV_WEEKDAY
- | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH);
+ DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_ABBREV_MONTH);
}
return headerDate;
}
@@ -103,13 +130,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
* Stops schedules row adapter.
*/
public void stop() {
- // TODO: Deal with other type of operation.
+ mHandler.removeCallbacksAndMessages(null);
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
for (int i = 0; i < size(); i++) {
if (get(i) instanceof ScheduleRow) {
- ScheduleRow scheduleRow = (ScheduleRow) get(i);
- if (scheduleRow.isRemoveScheduleChecked()) {
- TvApplication.getSingletons(mContext).getDvrManager()
- .removeScheduledRecording(scheduleRow.getRecording());
+ ScheduleRow row = (ScheduleRow) get(i);
+ if (row.isScheduleCanceled()) {
+ dvrManager.removeScheduledRecording(row.getSchedule());
}
}
}
@@ -124,8 +151,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
}
for (int i = 0; i < size(); i++) {
Object item = get(i);
- if (item instanceof ScheduleRow) {
- if (((ScheduleRow) item).getRecording().getId() == recording.getId()) {
+ if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) {
+ if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) {
return (ScheduleRow) item;
}
}
@@ -133,19 +160,32 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
return null;
}
- /**
- * Adds a {@link ScheduleRow} by {@link ScheduledRecording} and update
- * {@link SchedulesHeaderRow} information.
- */
- protected void addScheduleRow(ScheduledRecording recording) {
+ private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) {
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (!(item instanceof ScheduleRow)) {
+ continue;
+ }
+ ScheduleRow row = (ScheduleRow) item;
+ if (row.getSchedule() != null && row.isStartRecordingRequested()
+ && row.matchSchedule(schedule)) {
+ return row;
+ }
+ }
+ return null;
+ }
+
+ private void addScheduleRow(ScheduledRecording recording) {
+ // This method must not be called from inherited class.
+ SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class));
if (recording != null) {
int pre = -1;
int index = 0;
for (; index < size(); index++) {
if (get(index) instanceof ScheduleRow) {
ScheduleRow scheduleRow = (ScheduleRow) get(index);
- if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare(
- scheduleRow.getRecording(), recording) > 0) {
+ if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare(
+ scheduleRow.getSchedule(), recording) > 0) {
break;
}
pre = index;
@@ -157,11 +197,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
headerRow.setItemCount(headerRow.getItemCount() + 1);
ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
add(++pre, addedRow);
+ updateHeaderDescription(headerRow);
} else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) {
SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow();
headerRow.setItemCount(headerRow.getItemCount() + 1);
ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
add(index, addedRow);
+ updateHeaderDescription(headerRow);
} else {
SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine),
mContext.getResources().getQuantityString(
@@ -177,11 +219,11 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
}
- /**
- * Removes {@link ScheduleRow} and update {@link SchedulesHeaderRow} information.
- */
- protected void removeScheduleRow(ScheduleRow scheduleRow) {
+ private void removeScheduleRow(ScheduleRow scheduleRow) {
+ // This method must not be called from inherited class.
+ SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class));
if (scheduleRow != null) {
+ scheduleRow.setSchedule(null);
SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
remove(scheduleRow);
// Changes the count information of header which the removed row belongs to.
@@ -191,58 +233,193 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter {
if (headerRow.getItemCount() == 0) {
remove(headerRow);
} else {
- headerRow.setDescription(mContext.getResources().getQuantityString(
- R.plurals.dvr_schedules_section_subtitle,
- headerRow.getItemCount(), headerRow.getItemCount()));
replace(indexOf(headerRow), headerRow);
+ updateHeaderDescription(headerRow);
}
}
}
}
+ private void updateHeaderDescription(SchedulesHeaderRow headerRow) {
+ headerRow.setDescription(mContext.getResources().getQuantityString(
+ R.plurals.dvr_schedules_section_subtitle,
+ headerRow.getItemCount(), headerRow.getItemCount()));
+ }
+
/**
* Called when a schedule recording is added to dvr date manager.
*/
- public void onScheduledRecordingAdded(ScheduledRecording recording) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
- || recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- addScheduleRow(recording);
- notifyArrayItemRangeChanged(0, size());
+ public void onScheduledRecordingAdded(ScheduledRecording schedule) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule);
+ ScheduleRow row = findRowWithStartRequest(schedule);
+ // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED
+ // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS.
+ // It happens in a short time and causes blinking. To avoid this intermediate state change,
+ // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS
+ // instead of in this method.
+ if (row == null) {
+ addScheduleRow(schedule);
+ sendNextUpdateMessage(System.currentTimeMillis());
}
}
/**
* Called when a schedule recording is removed from dvr date manager.
*/
- public void onScheduledRecordingRemoved(ScheduledRecording recording) {
- ScheduleRow scheduleRow = findRowByScheduledRecording(recording);
- if (scheduleRow != null) {
- removeScheduleRow(scheduleRow);
+ public void onScheduledRecordingRemoved(ScheduledRecording schedule) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule);
+ ScheduleRow row = findRowByScheduledRecording(schedule);
+ if (row != null) {
+ removeScheduleRow(row);
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ sendNextUpdateMessage(System.currentTimeMillis());
}
- notifyArrayItemRangeChanged(0, size());
}
/**
* Called when a schedule recording is updated in dvr date manager.
*/
- public void onScheduledRecordingUpdated(ScheduledRecording recording) {
- ScheduleRow scheduleRow = findRowByScheduledRecording(recording);
- if (scheduleRow != null) {
- scheduleRow.setRecording(recording);
- if (!willBeKept(recording)) {
- removeScheduleRow(scheduleRow);
+ public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule);
+ ScheduleRow row = findRowByScheduledRecording(schedule);
+ if (row != null) {
+ if (conflictChange && isStartOrStopRequested()) {
+ // Delay the conflict update until it gets the response of the start/stop request.
+ // The purpose is to avoid the intermediate conflict change.
+ addPendingUpdate(row);
+ return;
+ }
+ if (row.isStopRecordingRequested()) {
+ // Wait until the recording is finished
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ row.setStopRecordingRequested(false);
+ if (!isStartOrStopRequested()) {
+ executePendingUpdate();
+ }
+ row.setSchedule(schedule);
+ }
+ } else {
+ row.setSchedule(schedule);
+ if (!willBeKept(schedule)) {
+ removeScheduleRow(row);
+ }
+ }
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ sendNextUpdateMessage(System.currentTimeMillis());
+ } else {
+ row = findRowWithStartRequest(schedule);
+ // When the start recording was requested, we give the highest priority. So it is
+ // guaranteed that the state will be changed from NOT_STARTED to the other state.
+ // Update the row with the next state not to show the intermediate state which causes
+ // blinking.
+ if (row != null
+ && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ // This can be called multiple times, so do not call
+ // ScheduleRow.setStartRecordingRequested(false) here.
+ row.setStartRecordingRequested(false);
+ if (!isStartOrStopRequested()) {
+ executePendingUpdate();
+ }
+ row.setSchedule(schedule);
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ sendNextUpdateMessage(System.currentTimeMillis());
}
- } else if (willBeKept(recording)) {
- addScheduleRow(recording);
}
- notifyArrayItemRangeChanged(0, size());
+ }
+
+ /**
+ * Checks if there is a row which requested start/stop recording.
+ */
+ protected boolean isStartOrStopRequested() {
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow) {
+ ScheduleRow row = (ScheduleRow) item;
+ if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Delays update of the row.
+ */
+ protected void addPendingUpdate(ScheduleRow row) {
+ mPendingUpdate.add(row);
+ }
+
+ /**
+ * Executes the pending updates.
+ */
+ protected void executePendingUpdate() {
+ for (ScheduleRow row : mPendingUpdate) {
+ int index = indexOf(row);
+ if (index != -1) {
+ notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ mPendingUpdate.clear();
}
/**
* To check whether the recording should be kept or not.
*/
- protected boolean willBeKept(ScheduledRecording recording) {
- return recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
- || recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
+ protected boolean willBeKept(ScheduledRecording schedule) {
+ // CANCELED state means that the schedule was removed temporarily, which should be shown
+ // in the list so that the user can reschedule it.
+ return schedule.getEndTimeMs() > System.currentTimeMillis()
+ && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED);
+ }
+
+ /**
+ * Handle the message to update/remove rows.
+ */
+ protected void handleUpdateRow(long currentTimeMs) {
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow) {
+ ScheduleRow row = (ScheduleRow) item;
+ if (row.getEndTimeMs() <= currentTimeMs) {
+ removeScheduleRow(row);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary.
+ */
+ protected long getNextTimerMs(long currentTimeMs) {
+ long earliest = Long.MAX_VALUE;
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow) {
+ // If the schedule was finished earlier than the end time, it should be removed
+ // when it reaches the end time in this class.
+ ScheduleRow row = (ScheduleRow) item;
+ if (earliest > row.getEndTimeMs()) {
+ earliest = row.getEndTimeMs();
+ }
+ }
+ }
+ return earliest;
+ }
+
+ /**
+ * Send update message at the time returned by {@link #getNextTimerMs}.
+ */
+ protected final void sendNextUpdateMessage(long currentTimeMs) {
+ mHandler.removeMessages(MSG_UPDATE_ROW);
+ long nextTime = getNextTimerMs(currentTimeMs);
+ if (nextTime != Long.MAX_VALUE) {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW,
+ nextTime - System.currentTimeMillis());
+ }
}
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 23aaf4c3..1257e725 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -16,17 +16,20 @@
package com.android.tv.dvr.ui.list;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.media.tv.TvInputInfo;
+import android.content.res.Resources;
+import android.os.Build;
+import android.support.annotation.IntDef;
import android.support.v17.leanback.widget.RowPresenter;
import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Range;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
@@ -37,140 +40,137 @@ import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
-import java.util.ArrayList;
-import java.util.HashMap;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* A RowPresenter for {@link ScheduleRow}.
*/
+@TargetApi(Build.VERSION_CODES.N)
public class ScheduleRowPresenter extends RowPresenter {
- private Context mContext;
- private Set<ScheduleRowClickListener> mListeners = new ArraySet<>();
- private final Drawable mBeingRecordedDrawable;
-
- private final Map<String, HashMap<Long, ScheduledRecording>> mInputScheduleMap = new
- HashMap<>();
- private final List<ScheduledRecording> mConflicts = new ArrayList<>();
- // TODO: Handle input schedule map and conflicts info in the adapter.
-
- private final Drawable mOnAirDrawable;
- private final Drawable mCancelDrawable;
- private final Drawable mScheduleDrawable;
+ private static final String TAG = "ScheduleRowPresenter";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ACTION_START_RECORDING, ACTION_STOP_RECORDING, ACTION_CREATE_SCHEDULE,
+ ACTION_REMOVE_SCHEDULE})
+ public @interface ScheduleRowAction {}
+ /** An action to start recording. */
+ public static final int ACTION_START_RECORDING = 1;
+ /** An action to stop recording. */
+ public static final int ACTION_STOP_RECORDING = 2;
+ /** An action to create schedule for the row. */
+ public static final int ACTION_CREATE_SCHEDULE = 3;
+ /** An action to remove the schedule. */
+ public static final int ACTION_REMOVE_SCHEDULE = 4;
+
+ private final Context mContext;
+ private final DvrManager mDvrManager;
+ private final DvrScheduleManager mDvrScheduleManager;
private final String mTunerConflictWillNotBeRecordedInfo;
private final String mTunerConflictWillBePartiallyRecordedInfo;
- private final String mInfoSeparator;
+ private final int mAnimationDuration;
+
+ private int mLastFocusedViewId;
/**
* A ViewHolder for {@link ScheduleRow}
*/
public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder {
+ private ScheduleRowPresenter mPresenter;
+ @ScheduleRowAction private int[] mActions;
private boolean mLtr;
private LinearLayout mInfoContainer;
- private RelativeLayout mScheduleActionContainer;
- private RelativeLayout mDeleteActionContainer;
+ // The first action is on the right of the second action.
+ private RelativeLayout mSecondActionContainer;
+ private RelativeLayout mFirstActionContainer;
private View mSelectorView;
private TextView mTimeView;
private TextView mProgramTitleView;
private TextView mInfoSeparatorView;
private TextView mChannelNameView;
private TextView mConflictInfoView;
- private ImageView mScheduleActionView;
- private ImageView mDeleteActionView;
-
- private ScheduledRecording mRecording;
+ private ImageView mSecondActionView;
+ private ImageView mFirstActionView;
+
+ private Runnable mPendingAnimationRunnable;
+
+ private final int mSelectorTranslationDelta;
+ private final int mSelectorWidthDelta;
+ private final int mInfoContainerTargetWidthWithNoAction;
+ private final int mInfoContainerTargetWidthWithOneAction;
+ private final int mInfoContainerTargetWidthWithTwoAction;
+ private final int mRoundRectRadius;
+
+ private final OnFocusChangeListener mOnFocusChangeListener =
+ new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean focused) {
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ if (view.isFocused()) {
+ mPresenter.mLastFocusedViewId = view.getId();
+ }
+ updateSelector();
+ }
+ });
+ }
+ };
- public ScheduleRowViewHolder(View view) {
+ public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) {
super(view);
+ mPresenter = presenter;
mLtr = view.getContext().getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_LTR;
mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container);
- mScheduleActionContainer = (RelativeLayout) view.findViewById(
- R.id.action_schedule_container);
- mScheduleActionView = (ImageView) view.findViewById(R.id.action_schedule);
- mDeleteActionContainer = (RelativeLayout) view.findViewById(
- R.id.action_delete_container);
- mDeleteActionView = (ImageView) view.findViewById(R.id.action_delete);
+ mSecondActionContainer = (RelativeLayout) view.findViewById(
+ R.id.action_second_container);
+ mSecondActionView = (ImageView) view.findViewById(R.id.action_second);
+ mFirstActionContainer = (RelativeLayout) view.findViewById(
+ R.id.action_first_container);
+ mFirstActionView = (ImageView) view.findViewById(R.id.action_first);
mSelectorView = view.findViewById(R.id.selector);
mTimeView = (TextView) view.findViewById(R.id.time);
mProgramTitleView = (TextView) view.findViewById(R.id.program_title);
mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator);
mChannelNameView = (TextView) view.findViewById(R.id.channel_name);
mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info);
-
- mInfoContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View view, boolean focused) {
- view.post(new Runnable() {
- @Override
- public void run() {
- updateSelector();
- }
- });
- }
- });
-
- mDeleteActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View view, boolean focused) {
- view.post(new Runnable() {
- @Override
- public void run() {
- updateSelector();
- }
- });
- }
- });
-
- mScheduleActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() {
- @Override
- public void onFocusChange(View view, boolean focused) {
- view.post(new Runnable() {
- @Override
- public void run() {
- updateSelector();
- }
- });
- }
- });
- }
-
- /**
- * Sets scheduled recording.
- */
- public void setRecording(ScheduledRecording recording) {
- mRecording = recording;
- }
-
- /**
- * Returns Info container.
- */
- public LinearLayout getInfoContainer() {
- return mInfoContainer;
- }
-
- /**
- * Returns schedule action container.
- */
- public RelativeLayout getScheduleActionContainer() {
- return mScheduleActionContainer;
- }
-
- /**
- * Returns delete action container.
- */
- public RelativeLayout getDeleteActionContainer() {
- return mDeleteActionContainer;
+ Resources res = view.getResources();
+ mSelectorTranslationDelta =
+ res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
+ - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_translation_delta);
+ mSelectorWidthDelta = res.getDimensionPixelSize(
+ R.dimen.dvr_schedules_item_focus_width_delta);
+ mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius);
+ int fullWidth = res.getDimensionPixelSize(
+ R.dimen.dvr_schedules_item_width)
+ - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding);
+ mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius;
+ mInfoContainerTargetWidthWithOneAction = fullWidth
+ - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
+ - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width)
+ + mRoundRectRadius + mSelectorWidthDelta;
+ mInfoContainerTargetWidthWithTwoAction = mInfoContainerTargetWidthWithOneAction
+ - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
+ - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size);
+
+ mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener);
+ mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener);
+ mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener);
}
/**
@@ -187,92 +187,55 @@ public class ScheduleRowPresenter extends RowPresenter {
return mProgramTitleView;
}
- /**
- * Returns subtitle view.
- */
- public TextView getChannelNameView() {
- return mChannelNameView;
- }
-
- /**
- * Returns conflict information view.
- */
- public TextView getConflictInfoView() {
- return mConflictInfoView;
- }
-
- /**
- * Returns schedule action view.
- */
- public ImageView getScheduleActionView() {
- return mScheduleActionView;
- }
-
- /**
- * Returns delete action view.
- */
- public ImageView getDeleteActionView() {
- return mDeleteActionView;
- }
-
- /**
- * Returns scheduled recording.
- */
- public ScheduledRecording getRecording() {
- return mRecording;
- }
-
private void updateSelector() {
- // TODO: Support RTL language
int animationDuration = mSelectorView.getResources().getInteger(
android.R.integer.config_shortAnimTime);
DecelerateInterpolator interpolator = new DecelerateInterpolator();
- int roundRectRadius = view.getResources().getDimensionPixelSize(
- R.dimen.dvr_schedules_selector_radius);
- if (mInfoContainer.isFocused() || mScheduleActionContainer.isFocused()
- || mDeleteActionContainer.isFocused()) {
+ if (mInfoContainer.isFocused() || mSecondActionContainer.isFocused()
+ || mFirstActionContainer.isFocused()) {
final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams();
final int targetWidth;
if (mInfoContainer.isFocused()) {
- if (mScheduleActionContainer.getVisibility() == View.GONE
- && mDeleteActionContainer.getVisibility() == View.GONE) {
- targetWidth = mInfoContainer.getWidth() + 2 * roundRectRadius;
+ // Use actions to check the visibility of the actions instead of calling
+ // View.getVisibility() because the view could be on the hiding animation.
+ if (mActions == null || mActions.length == 0) {
+ targetWidth = mInfoContainerTargetWidthWithNoAction;
+ } else if (mActions.length == 1) {
+ targetWidth = mInfoContainerTargetWidthWithOneAction;
} else {
- targetWidth = mInfoContainer.getWidth() + roundRectRadius;
- }
- } else if (mScheduleActionContainer.isFocused()) {
- if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) {
- targetWidth = mScheduleActionContainer.getWidth();
- } else {
- targetWidth = 2 * roundRectRadius;
+ targetWidth = mInfoContainerTargetWidthWithTwoAction;
}
+ } else if (mSecondActionContainer.isFocused()) {
+ targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius);
} else {
- targetWidth = mDeleteActionContainer.getWidth() + roundRectRadius;
+ targetWidth = mFirstActionContainer.getWidth() + mRoundRectRadius
+ + mSelectorTranslationDelta;
}
float targetTranslationX;
if (mInfoContainer.isFocused()) {
- targetTranslationX = mLtr ? mInfoContainer.getLeft() - roundRectRadius
+ targetTranslationX = mLtr ? mInfoContainer.getLeft() - mRoundRectRadius
- mSelectorView.getLeft() :
- mInfoContainer.getRight() + roundRectRadius - mInfoContainer.getRight();
- } else if (mScheduleActionContainer.isFocused()) {
- if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) {
- targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() -
+ mInfoContainer.getRight() + mRoundRectRadius - mSelectorView.getRight();
+ } else if (mSecondActionContainer.isFocused()) {
+ if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) {
+ targetTranslationX = mLtr ? mSecondActionContainer.getLeft() -
mSelectorView.getLeft()
- : mScheduleActionContainer.getRight() - mSelectorView.getRight();
+ : mSecondActionContainer.getRight() - mSelectorView.getRight();
} else {
- targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() -
- (roundRectRadius - mScheduleActionContainer.getWidth() / 2) -
+ targetTranslationX = mLtr ? mSecondActionContainer.getLeft() -
+ (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) -
mSelectorView.getLeft()
- : mScheduleActionContainer.getRight() +
- (roundRectRadius - mScheduleActionContainer.getWidth() / 2) -
+ : mSecondActionContainer.getRight() +
+ (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) -
mSelectorView.getRight();
}
} else {
- targetTranslationX = mLtr ? mDeleteActionContainer.getLeft()
- - mSelectorView.getLeft()
- : mDeleteActionContainer.getRight() - mSelectorView.getRight();
+ targetTranslationX = mLtr ? mFirstActionContainer.getLeft()
+ - mSelectorTranslationDelta - mSelectorView.getLeft()
+ : mFirstActionContainer.getRight() + mSelectorTranslationDelta
+ - mSelectorView.getRight();
}
if (mSelectorView.getAlpha() == 0) {
@@ -294,10 +257,14 @@ public class ScheduleRowPresenter extends RowPresenter {
mSelectorView.requestLayout();
}
}).setDuration(animationDuration).setInterpolator(interpolator).start();
+ if (mPendingAnimationRunnable != null) {
+ mPendingAnimationRunnable.run();
+ mPendingAnimationRunnable = null;
+ }
} else {
mSelectorView.animate().cancel();
mSelectorView.animate().alpha(0f).setDuration(animationDuration)
- .setInterpolator(interpolator).start();
+ .setInterpolator(interpolator).setUpdateListener(null).start();
}
}
@@ -338,23 +305,20 @@ public class ScheduleRowPresenter extends RowPresenter {
setHeaderPresenter(null);
setSelectEffectEnabled(false);
mContext = context;
- mBeingRecordedDrawable = mContext.getDrawable(R.drawable.ic_record_stop);
- mOnAirDrawable = mContext.getDrawable(R.drawable.ic_record_start);
- mCancelDrawable = mContext.getDrawable(R.drawable.ic_dvr_cancel);
- mScheduleDrawable = mContext.getDrawable(R.drawable.ic_scheduled_recording);
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager();
mTunerConflictWillNotBeRecordedInfo = mContext.getString(
R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info);
mTunerConflictWillBePartiallyRecordedInfo = mContext.getString(
R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded);
- mInfoSeparator = mContext.getString(R.string.dvr_schedules_information_separator);
- updateInputScheduleMap();
+ mAnimationDuration = mContext.getResources().getInteger(
+ android.R.integer.config_shortAnimTime);
}
@Override
public ViewHolder createRowViewHolder(ViewGroup parent) {
- View view = LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item,
- parent, false);
- return onGetScheduleRowViewHolder(view);
+ return onGetScheduleRowViewHolder(LayoutInflater.from(mContext)
+ .inflate(R.layout.dvr_schedules_item, parent, false));
}
/**
@@ -365,396 +329,467 @@ public class ScheduleRowPresenter extends RowPresenter {
}
/**
- * Returns be recorded drawable which is for being recorded scheduled recordings.
- */
- protected Drawable getBeingRecordedDrawable() {
- return mBeingRecordedDrawable;
- }
-
- /**
- * Returns on air drawable which is for on air but not being recorded scheduled recordings.
+ * Returns DVR manager.
*/
- protected Drawable getOnAirDrawable() {
- return mOnAirDrawable;
- }
-
- /**
- * Returns cancel drawable which is for cancelling scheduled recording.
- */
- protected Drawable getCancelDrawable() {
- return mCancelDrawable;
- }
-
- /**
- * Returns schedule drawable which is for scheduling.
- */
- protected Drawable getScheduleDrawable() {
- return mScheduleDrawable;
- }
-
- /**
- * Returns conflicting scheduled recordings.
- */
- protected List<ScheduledRecording> getConflicts() {
- return mConflicts;
+ protected DvrManager getDvrManager() {
+ return mDvrManager;
}
@Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
super.onBindRowViewHolder(vh, item);
ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
- ScheduleRow scheduleRow = (ScheduleRow) item;
- ScheduledRecording recording = scheduleRow.getRecording();
- // TODO: Do not show separator in the first row.
+ ScheduleRow row = (ScheduleRow) item;
+ @ScheduleRowAction int[] actions = getAvailableActions(row);
+ viewHolder.mActions = actions;
viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- onInfoClicked(scheduleRow);
+ onInfoClicked(row);
}
});
- viewHolder.mDeleteActionContainer.setOnClickListener(new View.OnClickListener() {
+ viewHolder.mFirstActionContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- onDeleteClicked(scheduleRow, viewHolder);
+ onActionClicked(actions[0], row);
}
});
- viewHolder.mScheduleActionContainer.setOnClickListener(new View.OnClickListener() {
+ viewHolder.mSecondActionContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- onScheduleClicked(scheduleRow);
+ onActionClicked(actions[1], row);
}
});
- viewHolder.mTimeView.setText(onGetRecordingTimeText(recording));
- Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager()
- .getChannel(recording.getChannelId());
- String programInfoText = onGetProgramInfoText(recording);
+ viewHolder.mTimeView.setText(onGetRecordingTimeText(row));
+ String programInfoText = onGetProgramInfoText(row);
if (TextUtils.isEmpty(programInfoText)) {
int durationMins =
- Math.max((int) TimeUnit.MILLISECONDS.toMinutes(recording.getDuration()), 1);
+ Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1);
programInfoText = mContext.getResources().getQuantityString(
R.plurals.dvr_schedules_recording_duration, durationMins, durationMins);
}
- String channelName = channel != null ? channel.getDisplayName() : null;
+ String channelName = getChannelNameText(row);
viewHolder.mProgramTitleView.setText(programInfoText);
viewHolder.mInfoSeparatorView.setVisibility((!TextUtils.isEmpty(programInfoText)
&& !TextUtils.isEmpty(channelName)) ? View.VISIBLE : View.GONE);
viewHolder.mChannelNameView.setText(channelName);
- if (!scheduleRow.isRemoveScheduleChecked()) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable);
- } else {
- viewHolder.mDeleteActionView.setImageDrawable(mCancelDrawable);
+ if (actions != null) {
+ switch (actions.length) {
+ case 2:
+ viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1]));
+ // pass through
+ case 1:
+ viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0]));
+ break;
}
- } else {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable);
+ }
+ if (mDvrManager.isConflicting(row.getSchedule())) {
+ String conflictInfo;
+ if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
+ conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
} else {
- viewHolder.mDeleteActionView.setImageDrawable(mScheduleDrawable);
+ conflictInfo = mTunerConflictWillNotBeRecordedInfo;
}
- viewHolder.mProgramTitleView.setTextColor(
- mContext.getResources().getColor(R.color.dvr_schedules_item_info, null));
+ viewHolder.mConflictInfoView.setText(conflictInfo);
+ viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
+ } else {
+ viewHolder.mConflictInfoView.setVisibility(View.GONE);
+ }
+ if (shouldBeGrayedOut(row)) {
+ viewHolder.greyOutInfo();
+ } else {
+ viewHolder.whiteBackInfo();
+ }
+ updateActionContainer(viewHolder, viewHolder.isSelected());
+ }
+
+ private int getImageForAction(@ScheduleRowAction int action) {
+ switch (action) {
+ case ACTION_START_RECORDING:
+ return R.drawable.ic_record_start;
+ case ACTION_STOP_RECORDING:
+ return R.drawable.ic_record_stop;
+ case ACTION_CREATE_SCHEDULE:
+ return R.drawable.ic_scheduled_recording;
+ case ACTION_REMOVE_SCHEDULE:
+ return R.drawable.ic_dvr_cancel;
+ default:
+ return 0;
}
- viewHolder.mRecording = recording;
- onBindRowViewHolderInternal(viewHolder, scheduleRow);
}
/**
* Returns view holder for schedule row.
*/
protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) {
- return new ScheduleRowViewHolder(view);
+ return new ScheduleRowViewHolder(view, this);
}
/**
* Returns time text for time view from scheduled recording.
*/
- protected String onGetRecordingTimeText(ScheduledRecording recording) {
- return Utils.getDurationString(mContext, recording.getStartTimeMs(),
- recording.getEndTimeMs(), true, false, true, 0);
+ protected String onGetRecordingTimeText(ScheduleRow row) {
+ return Utils.getDurationString(mContext, row.getStartTimeMs(), row.getEndTimeMs(), true,
+ false, true, 0);
}
/**
* Returns program info text for program title view.
*/
- protected String onGetProgramInfoText(ScheduledRecording recording) {
- if (recording != null) {
- return recording.getProgramTitle();
- }
- return null;
+ protected String onGetProgramInfoText(ScheduleRow row) {
+ return row.getProgramTitleWithEpisodeNumber(mContext);
}
- /**
- * Internal method for onBindRowViewHolder, can be customized by subclass.
- */
- protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder, ScheduleRow
- scheduleRow) {
- if (mConflicts.contains(scheduleRow.getRecording())) {
- viewHolder.mScheduleActionView.setImageDrawable(mScheduleDrawable);
- String conflictInfo = mTunerConflictWillNotBeRecordedInfo;
- // TODO: It's also possible for the NonStarted schedules to be partially recorded.
- if (viewHolder.mRecording.getState()
- == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
- }
- viewHolder.mConflictInfoView.setText(conflictInfo);
- // TODO: Add 12dp warning icon to conflict info.
- viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
- viewHolder.greyOutInfo();
- } else {
- viewHolder.mScheduleActionContainer.setVisibility(View.GONE);
- viewHolder.mConflictInfoView.setVisibility(View.GONE);
- if (!scheduleRow.isRemoveScheduleChecked()) {
- viewHolder.whiteBackInfo();
- }
- }
+ private String getChannelNameText(ScheduleRow row) {
+ Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager()
+ .getChannel(row.getChannelId());
+ return channel == null ? null :
+ TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() :
+ channel.getDisplayName().trim() + " " + channel.getDisplayNumber();
}
/**
- * Updates input schedule map.
+ * Called when user click Info in {@link ScheduleRow}.
*/
- private void updateInputScheduleMap() {
- mInputScheduleMap.clear();
- List<ScheduledRecording> allRecordings = TvApplication.getSingletons(getContext())
- .getDvrDataManager().getAvailableScheduledRecordings();
- for(ScheduledRecording recording : allRecordings) {
- addScheduledRecordingToMap(recording);
+ protected void onInfoClicked(ScheduleRow scheduleRow) {
+ ScheduledRecording schedule = scheduleRow.getSchedule();
+ if (schedule != null) {
+ DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true);
}
- updateConflicts();
}
/**
- * Updates conflicting scheduled recordings.
+ * Called when the button in a row is clicked.
*/
- private void updateConflicts() {
- mConflicts.clear();
- for (String inputId : mInputScheduleMap.keySet()) {
- TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId);
- if (input == null) {
- continue;
- }
- mConflicts.addAll(DvrScheduleManager.getConflictingSchedules(
- new ArrayList<>(mInputScheduleMap.get(inputId).values()),
- input.getTunerCount()));
+ protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) {
+ switch (action) {
+ case ACTION_START_RECORDING:
+ onStartRecording(row);
+ break;
+ case ACTION_STOP_RECORDING:
+ onStopRecording(row);
+ break;
+ case ACTION_CREATE_SCHEDULE:
+ onCreateSchedule(row);
+ break;
+ case ACTION_REMOVE_SCHEDULE:
+ onRemoveSchedule(row);
+ break;
}
}
/**
- * Adds a scheduled recording to the map, it happens when user undo cancel.
+ * Action handler for {@link #ACTION_START_RECORDING}.
*/
- private void addScheduledRecordingToMap(ScheduledRecording recording) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext,
- recording.getChannelId());
- if (input == null) {
+ protected void onStartRecording(ScheduleRow row) {
+ ScheduledRecording schedule = row.getSchedule();
+ if (schedule == null) {
+ // This row has been deleted.
return;
}
- String inputId = input.getId();
- HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId);
- if (schedulesMap == null) {
- schedulesMap = new HashMap<>();
- mInputScheduleMap.put(inputId, schedulesMap);
+ // Checks if there are current recordings that will be stopped by schedule this program.
+ // If so, shows confirmation dialog to users.
+ List<ScheduledRecording> conflictSchedules = mDvrScheduleManager.getConflictingSchedules(
+ schedule.getChannelId(), System.currentTimeMillis(), schedule.getEndTimeMs());
+ for (int i = conflictSchedules.size() - 1; i >= 0; i--) {
+ ScheduledRecording conflictSchedule = conflictSchedules.get(i);
+ if (conflictSchedule.isInProgress()) {
+ DvrUiHelper.showStopRecordingDialog((Activity) mContext,
+ conflictSchedule.getChannelId(),
+ DvrStopRecordingFragment.REASON_ON_CONFLICT,
+ new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
+ onStartRecordingInternal(row);
+ }
+ }
+ });
+ return;
+ }
}
- schedulesMap.put(recording.getId(), recording);
+ onStartRecordingInternal(row);
}
- /**
- * Called when a scheduled recording is added into dvr date manager.
- */
- public void onScheduledRecordingAdded(ScheduledRecording recording) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || recording
- .getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- addScheduledRecordingToMap(recording);
- updateConflicts();
+ private void onStartRecordingInternal(ScheduleRow row) {
+ if (row.isOnAir() && !row.isRecordingInProgress() && !row.isStartRecordingRequested()) {
+ row.setStartRecordingRequested(true);
+ if (row.isRecordingNotStarted()) {
+ mDvrManager.setHighestPriority(row.getSchedule());
+ } else if (row.isRecordingFinished()) {
+ mDvrManager.addSchedule(ScheduledRecording.buildFrom(row.getSchedule())
+ .setId(ScheduledRecording.ID_NOT_SET)
+ .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
+ .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule()))
+ .build());
+ } else {
+ SoftPreconditions.checkState(false, TAG, "Invalid row state to start recording: "
+ + row);
+ return;
+ }
+ String msg = mContext.getString(R.string.dvr_msg_current_program_scheduled,
+ row.getSchedule().getProgramTitle(),
+ Utils.toTimeString(row.getEndTimeMs(), false));
+ ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT);
}
}
/**
- * Adds a scheduled recording to the map, it happens when user undo cancel.
+ * Action handler for {@link #ACTION_STOP_RECORDING}.
*/
- private void updateScheduledRecordingToMap(ScheduledRecording recording) {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED ||
- recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext,
- recording.getChannelId());
- if (input == null) {
- return;
- }
- String inputId = input.getId();
- HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId);
- if (schedulesMap == null) {
- addScheduledRecordingToMap(recording);
- return;
+ protected void onStopRecording(ScheduleRow row) {
+ if (row.getSchedule() == null) {
+ // This row has been deleted.
+ return;
+ }
+ if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
+ row.setStopRecordingRequested(true);
+ mDvrManager.stopRecording(row.getSchedule());
+ CharSequence deletedInfo = onGetProgramInfoText(row);
+ if (TextUtils.isEmpty(deletedInfo)) {
+ deletedInfo = getChannelNameText(row);
}
- schedulesMap.put(recording.getId(), recording);
- } else {
- removeScheduledRecordingFromMap(recording);
+ ToastUtils.show(mContext, mContext.getResources()
+ .getString(R.string.dvr_schedules_deletion_info, deletedInfo),
+ Toast.LENGTH_SHORT);
}
}
/**
- * Called when a scheduled recording is updated in dvr date manager.
+ * Action handler for {@link #ACTION_CREATE_SCHEDULE}.
*/
- public void onScheduledRecordingUpdated(ScheduledRecording recording) {
- updateScheduledRecordingToMap(recording);
- updateConflicts();
+ protected void onCreateSchedule(ScheduleRow row) {
+ if (row.getSchedule() == null) {
+ // This row has been deleted.
+ return;
+ }
+ if (!row.isOnAir()) {
+ if (row.isScheduleCanceled()) {
+ mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule())
+ .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
+ .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule()))
+ .build());
+ String msg = mContext.getString(R.string.dvr_msg_program_scheduled,
+ row.getSchedule().getProgramTitle());
+ ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT);
+ } else if (mDvrManager.isConflicting(row.getSchedule())) {
+ mDvrManager.setHighestPriority(row.getSchedule());
+ }
+ }
}
/**
- * Removes a scheduled recording from the map, it happens when user cancel schedule.
+ * Action handler for {@link #ACTION_REMOVE_SCHEDULE}.
*/
- private void removeScheduledRecordingFromMap(ScheduledRecording recording) {
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, recording.getChannelId());
- if (input == null) {
+ protected void onRemoveSchedule(ScheduleRow row) {
+ if (row.getSchedule() == null) {
+ // This row has been deleted.
return;
}
- String inputId = input.getId();
- HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId);
- if (schedulesMap == null) {
- return;
+ CharSequence deletedInfo = null;
+ if (row.isOnAir()) {
+ if (row.isRecordingNotStarted()) {
+ deletedInfo = getDeletedInfo(row);
+ mDvrManager.removeScheduledRecording(row.getSchedule());
+ }
+ } else {
+ if (mDvrManager.isConflicting(row.getSchedule())
+ && !shouldKeepScheduleAfterRemoving()) {
+ deletedInfo = getDeletedInfo(row);
+ mDvrManager.removeScheduledRecording(row.getSchedule());
+ } else if (row.isRecordingNotStarted()) {
+ deletedInfo = getDeletedInfo(row);
+ mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule())
+ .setState(ScheduledRecording.STATE_RECORDING_CANCELED)
+ .build());
+ }
}
- schedulesMap.remove(recording.getId());
- if (schedulesMap.isEmpty()) {
- mInputScheduleMap.remove(inputId);
+ if (deletedInfo != null) {
+ ToastUtils.show(mContext, mContext.getResources()
+ .getString(R.string.dvr_schedules_deletion_info, deletedInfo),
+ Toast.LENGTH_SHORT);
}
}
- /**
- * Called when a scheduled recording is removed from dvr date manager.
- */
- public void onScheduledRecordingRemoved(ScheduledRecording recording) {
- removeScheduledRecordingFromMap(recording);
- updateConflicts();
+ private CharSequence getDeletedInfo(ScheduleRow row) {
+ CharSequence deletedInfo = onGetProgramInfoText(row);
+ if (TextUtils.isEmpty(deletedInfo)) {
+ return getChannelNameText(row);
+ }
+ return deletedInfo;
}
- /**
- * Called when user click Info in {@link ScheduleRow}.
- */
- protected void onInfoClicked(ScheduleRow scheduleRow) {
- DvrUiHelper.startDetailsActivity((Activity) mContext,
- scheduleRow.getRecording(), null, true);
+ @Override
+ protected void onRowViewSelected(ViewHolder vh, boolean selected) {
+ super.onRowViewSelected(vh, selected);
+ updateActionContainer(vh, selected);
}
/**
- * Called when user click schedule in {@link ScheduleRow}.
+ * Internal method for onRowViewSelected, can be customized by subclass.
*/
- protected void onScheduleClicked(ScheduleRow scheduleRow) {
- ScheduledRecording scheduledRecording = scheduleRow.getRecording();
- TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext,
- scheduledRecording.getChannelId());
- if (input == null) {
- return;
- }
- List<ScheduledRecording> allScheduledRecordings = new ArrayList<ScheduledRecording>(
- mInputScheduleMap.get(input.getId()).values());
- long maxPriority = scheduledRecording.getPriority();
- for (ScheduledRecording recording : allScheduledRecordings) {
- if (scheduledRecording.isOverLapping(
- new Range<>(recording.getStartTimeMs(), recording.getEndTimeMs()))) {
- if (maxPriority < recording.getPriority()) {
- maxPriority = recording.getPriority();
+ private void updateActionContainer(ViewHolder vh, boolean selected) {
+ ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
+ viewHolder.mSecondActionContainer.animate().setListener(null).cancel();
+ viewHolder.mFirstActionContainer.animate().setListener(null).cancel();
+ if (selected && viewHolder.mActions != null) {
+ switch (viewHolder.mActions.length) {
+ case 2:
+ prepareShowActionView(viewHolder.mSecondActionContainer);
+ prepareShowActionView(viewHolder.mFirstActionContainer);
+ viewHolder.mPendingAnimationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ showActionView(viewHolder.mSecondActionContainer);
+ showActionView(viewHolder.mFirstActionContainer);
+ }
+ };
+ break;
+ case 1:
+ prepareShowActionView(viewHolder.mFirstActionContainer);
+ viewHolder.mPendingAnimationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ showActionView(viewHolder.mFirstActionContainer);
+ }
+ };
+ if (mLastFocusedViewId == R.id.action_second_container) {
+ mLastFocusedViewId = R.id.info_container;
+ }
+ break;
+ case 0:
+ default:
+ viewHolder.mPendingAnimationRunnable = new Runnable() {
+ @Override
+ public void run() {
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ hideActionView(viewHolder.mFirstActionContainer, View.GONE);
+ }
+ };
+ if (mLastFocusedViewId == R.id.action_first_container
+ || mLastFocusedViewId == R.id.action_second_container) {
+ mLastFocusedViewId = R.id.info_container;
+ }
+ break;
+ }
+ View view = viewHolder.view.findViewById(mLastFocusedViewId);
+ if (view != null && view.getVisibility() == View.VISIBLE) {
+ // When the row is selected, information container gets the initial focus.
+ // To give the focus to the same control as the previous row, we need to call
+ // requestFocus() explicitly.
+ if (view.hasFocus()) {
+ viewHolder.mPendingAnimationRunnable.run();
+ } else {
+ view.requestFocus();
}
}
+ } else {
+ viewHolder.mPendingAnimationRunnable = null;
+ hideActionView(viewHolder.mFirstActionContainer, View.GONE);
+ hideActionView(viewHolder.mSecondActionContainer, View.GONE);
+ }
+ }
+
+ private void prepareShowActionView(View view) {
+ if (view.getVisibility() != View.VISIBLE) {
+ view.setAlpha(0.0f);
}
- TvApplication.getSingletons(getContext()).getDvrManager()
- .updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording)
- .setPriority(maxPriority + 1).build());
- updateConflicts();
+ view.setVisibility(View.VISIBLE);
}
/**
- * Called when user click delete in {@link ScheduleRow}.
+ * Add animation when view is visible.
*/
- protected void onDeleteClicked(ScheduleRow scheduleRow, ViewHolder vh) {
- ScheduledRecording recording = scheduleRow.getRecording();
- ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
- if (!scheduleRow.isRemoveScheduleChecked()) {
- if (mConflicts.contains(recording)) {
- TvApplication.getSingletons(mContext)
- .getDvrManager().removeScheduledRecording(recording);
- }
+ private void showActionView(View view) {
+ view.animate().alpha(1.0f).setInterpolator(new DecelerateInterpolator())
+ .setDuration(mAnimationDuration).start();
+ }
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable);
- // TODO: Replace an icon whose size is the same as scheudle.
- } else {
- viewHolder.getDeleteActionView().setImageDrawable(mScheduleDrawable);
- }
- viewHolder.greyOutInfo();
- scheduleRow.setRemoveScheduleChecked(true);
- CharSequence deletedInfo = viewHolder.getProgramTitleView().getText();
- if (TextUtils.isEmpty(deletedInfo)) {
- deletedInfo = viewHolder.getChannelNameView().getText();
- }
- Toast.makeText(mContext, mContext.getResources()
- .getString(R.string.dvr_schedules_deletion_info, deletedInfo),
- Toast.LENGTH_SHORT).show();
- removeScheduledRecordingFromMap(recording);
- } else {
- if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
- viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable);
- // TODO: Replace an icon whose size is the same as scheudle.
- } else {
- viewHolder.getDeleteActionView().setImageDrawable(mCancelDrawable);
+ /**
+ * Add animation when view change to invisible.
+ */
+ private void hideActionView(View view, int visibility) {
+ if (view.getVisibility() != View.VISIBLE) {
+ if (view.getVisibility() != visibility) {
+ view.setVisibility(visibility);
}
- viewHolder.whiteBackInfo();
- scheduleRow.setRemoveScheduleChecked(false);
- addScheduledRecordingToMap(recording);
- }
- updateConflicts();
- for (ScheduleRowClickListener l : mListeners) {
- l.onDeleteClicked(scheduleRow);
+ return;
}
+ view.animate().alpha(0.0f).setInterpolator(new DecelerateInterpolator())
+ .setDuration(mAnimationDuration)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setVisibility(visibility);
+ view.animate().setListener(null);
+ }
+ }).start();
}
/**
- * Adds {@link ScheduleRowClickListener}.
+ * Returns the available actions according to the row's state. It should be the reverse order
+ * with that in the screen.
*/
- public void addListener(ScheduleRowClickListener scheduleRowClickListener) {
- mListeners.add(scheduleRowClickListener);
+ @ScheduleRowAction
+ protected int[] getAvailableActions(ScheduleRow row) {
+ if (row.getSchedule() != null) {
+ if (row.isOnAir()) {
+ if (row.isRecordingInProgress()) {
+ return new int[] {ACTION_STOP_RECORDING};
+ } else if (row.isRecordingNotStarted()) {
+ if (canResolveConflict()) {
+ // The "START" action can change the conflict states.
+ return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING};
+ } else {
+ return new int[] {ACTION_REMOVE_SCHEDULE};
+ }
+ } else if (row.isRecordingFinished()) {
+ return new int[] {ACTION_START_RECORDING};
+ } else {
+ SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the"
+ + " available actions(on air): " + row);
+ }
+ } else {
+ if (row.isScheduleCanceled()) {
+ return new int[] {ACTION_CREATE_SCHEDULE};
+ } else if (mDvrManager.isConflicting(row.getSchedule()) && canResolveConflict()) {
+ return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE};
+ } else if (row.isRecordingNotStarted()) {
+ return new int[] {ACTION_REMOVE_SCHEDULE};
+ } else {
+ SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the"
+ + " available actions(future schedule): " + row);
+ }
+ }
+ }
+ return null;
}
/**
- * Removes {@link ScheduleRowClickListener}.
+ * Check if the conflict can be resolved in this screen.
*/
- public void removeListener(ScheduleRowClickListener
- scheduleRowClickListener) {
- mListeners.remove(scheduleRowClickListener);
- }
-
- @Override
- protected void onRowViewSelected(ViewHolder vh, boolean selected) {
- super.onRowViewSelected(vh, selected);
- onRowViewSelectedInternal(vh, selected);
+ protected boolean canResolveConflict() {
+ return true;
}
/**
- * Internal method for onRowViewSelected, can be customized by subclass.
+ * Check if the schedule should be kept after removing it.
*/
- protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) {
- ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
- boolean isRecordingConflicting = mConflicts.contains(viewHolder.mRecording);
- if (selected) {
- viewHolder.mDeleteActionContainer.setVisibility(View.VISIBLE);
- if (isRecordingConflicting) {
- viewHolder.mScheduleActionContainer.setVisibility(View.VISIBLE);
- }
- } else {
- viewHolder.mDeleteActionContainer.setVisibility(View.GONE);
- if (isRecordingConflicting) {
- viewHolder.mScheduleActionContainer.setVisibility(View.GONE);
- }
- }
+ protected boolean shouldKeepScheduleAfterRemoving() {
+ return false;
}
/**
- * A listener for clicking {@link ScheduleRow}.
+ * Checks if the row should be grayed out.
*/
- public interface ScheduleRowClickListener{
- /**
- * To notify other observers that delete button has been clicked.
- */
- void onDeleteClicked(ScheduleRow scheduleRow);
+ protected boolean shouldBeGrayedOut(ScheduleRow row) {
+ return row.getSchedule() == null
+ || (row.isOnAir() && !row.isRecordingInProgress())
+ || mDvrManager.isConflicting(row.getSchedule())
+ || row.isScheduleCanceled();
}
}
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
index d103a533..0fb0924d 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
@@ -16,8 +16,6 @@
package com.android.tv.dvr.ui.list;
-import android.support.annotation.Nullable;
-
import com.android.tv.dvr.SeriesRecording;
/**
@@ -88,13 +86,6 @@ public abstract class SchedulesHeaderRow {
}
/**
- * Sets the latest time of the list which belongs to the header row.
- */
- public void setDeadLineMs(long deadLineMs) {
- mDeadLineMs = deadLineMs;
- }
-
- /**
* Returns the latest time of the list which belongs to the header row.
*/
public long getDeadLineMs() {
@@ -106,35 +97,26 @@ public abstract class SchedulesHeaderRow {
* The header row which represent the series recording.
*/
public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow {
- private SeriesRecording mSeries;
- private boolean mCancelAllChecked;
+ private SeriesRecording mSeriesRecording;
public SeriesRecordingHeaderRow(String title, String description, int itemCount,
SeriesRecording series) {
super(title, description, itemCount);
- mSeries = series;
- mCancelAllChecked = series.getState() == SeriesRecording.STATE_SERIES_CANCELED;
- }
-
- /**
- * Sets cancel all checked status.
- */
- public void setCancelAllChecked(boolean checked) {
- mCancelAllChecked = checked;
+ mSeriesRecording = series;
}
/**
- * Returns cancel all checked status.
+ * Returns the series recording, it is for series schedules list.
*/
- public boolean isCancelAllChecked() {
- return mCancelAllChecked;
+ public SeriesRecording getSeriesRecording() {
+ return mSeriesRecording;
}
/**
- * Returns the series recording, it is for series schedules list.
+ * Sets the series recording.
*/
- public SeriesRecording getSeriesRecording() {
- return mSeries;
+ public void setSeriesRecording(SeriesRecording seriesRecording) {
+ mSeriesRecording = seriesRecording;
}
}
}
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index 483962e7..69c33a96 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -20,7 +20,6 @@ import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.RowPresenter;
-import android.util.ArraySet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -36,14 +35,11 @@ import com.android.tv.dvr.SeriesRecording;
import com.android.tv.dvr.ui.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
-import java.util.Set;
-
/**
* A base class for RowPresenter for {@link SchedulesHeaderRow}
*/
public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
private Context mContext;
- private Set<SchedulesHeaderRowListener> mListeners = new ArraySet<>();
public SchedulesHeaderRowPresenter(Context context) {
setHeaderPresenter(null);
@@ -59,26 +55,6 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
/**
- * Adds {@link SchedulesHeaderRowListener}.
- */
- public void addListener(SchedulesHeaderRowListener listener) {
- mListeners.add(listener);
- }
-
- /**
- * Removes {@link SchedulesHeaderRowListener}.
- */
- public void removeListener(SchedulesHeaderRowListener listener) {
- mListeners.remove(listener);
- }
-
- void notifyUpdateAllScheduleRows() {
- for (SchedulesHeaderRowListener listener : mListeners) {
- listener.onUpdateAllScheduleRows();
- }
- }
-
- /**
* A ViewHolder for {@link SchedulesHeaderRow}.
*/
public static class SchedulesHeaderRowViewHolder extends RowPresenter.ViewHolder {
@@ -147,54 +123,59 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
mCancelDrawable = context.getDrawable(R.drawable.ic_dvr_cancel_large);
mResumeDrawable = context.getDrawable(R.drawable.ic_record_start);
mSettingsInfo = context.getString(R.string.dvr_series_schedules_settings);
- mCancelAllInfo = context.getString(R.string.dvr_series_schedules_cancel_all);
- mResumeInfo = context.getString(R.string.dvr_series_schedules_resume);
+ mCancelAllInfo = context.getString(R.string.dvr_series_schedules_stop);
+ mResumeInfo = context.getString(R.string.dvr_series_schedules_start);
}
@Override
protected ViewHolder createRowViewHolder(ViewGroup parent) {
- return new SeriesRecordingRowViewHolder(getContext(), parent);
+ return new SeriesHeaderRowViewHolder(getContext(), parent);
}
@Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) {
super.onBindRowViewHolder(viewHolder, item);
- SeriesRecordingRowViewHolder headerViewHolder =
- (SeriesRecordingRowViewHolder) viewHolder;
+ SeriesHeaderRowViewHolder headerViewHolder =
+ (SeriesHeaderRowViewHolder) viewHolder;
SeriesRecordingHeaderRow header = (SeriesRecordingHeaderRow) item;
headerViewHolder.mSeriesSettingsButton.setVisibility(
- isSeriesScheduleCanceled(getContext(), header) ? View.INVISIBLE : View.VISIBLE);
+ header.getSeriesRecording().isStopped() ? View.INVISIBLE : View.VISIBLE);
headerViewHolder.mSeriesSettingsButton.setText(mSettingsInfo);
setTextDrawable(headerViewHolder.mSeriesSettingsButton, mSettingsDrawable);
- if (header.isCancelAllChecked()) {
- headerViewHolder.mTogglePauseButton.setText(mResumeInfo);
- setTextDrawable(headerViewHolder.mTogglePauseButton, mResumeDrawable);
+ if (header.getSeriesRecording().isStopped()) {
+ headerViewHolder.mToggleStartStopButton.setText(mResumeInfo);
+ setTextDrawable(headerViewHolder.mToggleStartStopButton, mResumeDrawable);
} else {
- headerViewHolder.mTogglePauseButton.setText(mCancelAllInfo);
- setTextDrawable(headerViewHolder.mTogglePauseButton, mCancelDrawable);
+ headerViewHolder.mToggleStartStopButton.setText(mCancelAllInfo);
+ setTextDrawable(headerViewHolder.mToggleStartStopButton, mCancelDrawable);
}
headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
+ // TODO: pass channel list for settings.
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- header.getSeriesRecording().getId());
+ header.getSeriesRecording().getId(), null, false, false, false);
}
});
- headerViewHolder.mTogglePauseButton.setOnClickListener(new OnClickListener() {
+ headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
- if (!header.isCancelAllChecked()) {
- DvrUiHelper.showCancelAllSeriesRecordingDialog((DvrSchedulesActivity) view
- .getContext());
+ if (header.getSeriesRecording().isStopped()) {
+ // Reset priority to the highest.
+ SeriesRecording seriesRecording = SeriesRecording
+ .buildFrom(header.getSeriesRecording())
+ .setPriority(TvApplication.getSingletons(getContext())
+ .getDvrScheduleManager().suggestNewSeriesPriority())
+ .build();
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .updateSeriesRecording(seriesRecording);
+ // TODO: pass channel list for settings.
+ DvrUiHelper.startSeriesSettingsActivity(getContext(),
+ header.getSeriesRecording().getId(), null, false, false, false);
} else {
- if (isSeriesScheduleCanceled(getContext(), header)) {
- TvApplication.getSingletons(getContext()).getDvrManager()
- .updateSeriesRecording(SeriesRecording.buildFrom(header
- .getSeriesRecording()).setState(SeriesRecording
- .STATE_SERIES_NORMAL).build());
- }
- header.setCancelAllChecked(false);
- notifyUpdateAllScheduleRows();
+ DvrUiHelper.showCancelAllSeriesRecordingDialog(
+ (DvrSchedulesActivity) view.getContext(),
+ header.getSeriesRecording());
}
}
});
@@ -208,97 +189,85 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
}
- private static boolean isSeriesScheduleCanceled(Context context,
- SeriesRecordingHeaderRow header) {
- return TvApplication.getSingletons(context).getDvrDataManager()
- .getSeriesRecording(header.getSeriesRecording().getId()).getState()
- == SeriesRecording.STATE_SERIES_CANCELED;
- }
-
/**
* A ViewHolder for {@link SeriesRecordingHeaderRow}.
*/
- public static class SeriesRecordingRowViewHolder extends SchedulesHeaderRowViewHolder {
+ public static class SeriesHeaderRowViewHolder extends SchedulesHeaderRowViewHolder {
private final TextView mSeriesSettingsButton;
- private final TextView mTogglePauseButton;
+ private final TextView mToggleStartStopButton;
private final boolean mLtr;
private final View mSelector;
private View mLastFocusedView;
- public SeriesRecordingRowViewHolder(Context context, ViewGroup parent) {
+ public SeriesHeaderRowViewHolder(Context context, ViewGroup parent) {
super(context, parent);
mLtr = context.getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_LTR;
view.findViewById(R.id.button_container).setVisibility(View.VISIBLE);
mSeriesSettingsButton = (TextView) view.findViewById(R.id.series_settings);
- mTogglePauseButton = (TextView) view.findViewById(R.id.series_toggle_pause);
+ mToggleStartStopButton =
+ (TextView) view.findViewById(R.id.series_toggle_start_stop);
mSelector = view.findViewById(R.id.selector);
OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean focused) {
- onIconFouseChange(view);
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ updateSelector(view);
+ }
+ });
}
};
mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener);
- mTogglePauseButton.setOnFocusChangeListener(onFocusChangeListener);
- }
-
- void onIconFouseChange(View focusedView) {
- updateSelector(focusedView, mSelector);
+ mToggleStartStopButton.setOnFocusChangeListener(onFocusChangeListener);
}
- private void updateSelector(View focusedView, final View selectorView) {
- int animationDuration = selectorView.getContext().getResources()
+ private void updateSelector(View focusedView) {
+ int animationDuration = mSelector.getContext().getResources()
.getInteger(android.R.integer.config_shortAnimTime);
DecelerateInterpolator interpolator = new DecelerateInterpolator();
if (focusedView.hasFocus()) {
- final ViewGroup.LayoutParams lp = selectorView.getLayoutParams();
+ ViewGroup.LayoutParams lp = mSelector.getLayoutParams();
final int targetWidth = focusedView.getWidth();
float targetTranslationX;
if (mLtr) {
- targetTranslationX = focusedView.getLeft() - selectorView.getLeft();
+ targetTranslationX = focusedView.getLeft() - mSelector.getLeft();
} else {
- targetTranslationX = focusedView.getRight() - selectorView.getRight();
+ targetTranslationX = focusedView.getRight() - mSelector.getRight();
}
// if the selector is invisible, set the width and translation X directly -
// don't animate.
- if (selectorView.getAlpha() == 0) {
- selectorView.setTranslationX(targetTranslationX);
+ if (mSelector.getAlpha() == 0) {
+ mSelector.setTranslationX(targetTranslationX);
lp.width = targetWidth;
- selectorView.requestLayout();
+ mSelector.requestLayout();
}
// animate the selector in and to the proper width and translation X.
final float deltaWidth = lp.width - targetWidth;
- selectorView.animate().cancel();
- selectorView.animate().translationX(targetTranslationX).alpha(1f)
+ mSelector.animate().cancel();
+ mSelector.animate().translationX(targetTranslationX).alpha(1f)
.setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// Set width to the proper width for this animation step.
lp.width = targetWidth + Math.round(
deltaWidth * (1f - animation.getAnimatedFraction()));
- selectorView.requestLayout();
+ mSelector.requestLayout();
}
}).setDuration(animationDuration).setInterpolator(interpolator).start();
mLastFocusedView = focusedView;
} else if (mLastFocusedView == focusedView) {
- selectorView.animate().cancel();
- selectorView.animate().alpha(0f).setDuration(animationDuration)
+ mSelector.animate().setUpdateListener(null).cancel();
+ mSelector.animate().alpha(0f).setDuration(animationDuration)
.setInterpolator(interpolator).start();
mLastFocusedView = null;
}
}
}
}
-
- public interface SchedulesHeaderRowListener {
- /**
- * Updates all schedule rows.
- */
- void onUpdateAllScheduleRows();
- }
}
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
index 8b162c54..3b493774 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -16,12 +16,20 @@
package com.android.tv.dvr.ui.list;
+import android.annotation.TargetApi;
import android.content.Context;
+import android.media.tv.TvInputInfo;
+import android.os.Build;
import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.SeriesRecording;
@@ -30,127 +38,232 @@ import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
/**
* An adapter for series schedule row.
*/
+@TargetApi(Build.VERSION_CODES.N)
public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
- private static final String TAG = "SeriesScheduleRowAdapter";
+ private static final String TAG = "SeriesRowAdapter";
+ private static final boolean DEBUG = false;
- private SeriesRecording mSeriesRecording;
+ private final SeriesRecording mSeriesRecording;
+ private final String mInputId;
+ private final DvrManager mDvrManager;
+ private final DvrDataManager mDataManager;
+ private final Map<Long, Program> mPrograms = new ArrayMap<>();
+ private SeriesRecordingHeaderRow mHeaderRow;
- public SeriesScheduleRowAdapter(Context context,
- ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording) {
+ public SeriesScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector,
+ SeriesRecording seriesRecording) {
super(context, classPresenterSelector);
mSeriesRecording = seriesRecording;
+ TvInputInfo input = Utils.getTvInputInfoForInputId(context, mSeriesRecording.getInputId());
+ if (SoftPreconditions.checkNotNull(input) != null) {
+ mInputId = input.getId();
+ } else {
+ mInputId = null;
+ }
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mDvrManager = singletons.getDvrManager();
+ mDataManager = singletons.getDvrDataManager();
+ setHasStableIds(true);
}
@Override
public void start() {
- List<ScheduledRecording> recordings = TvApplication.getSingletons(getContext())
- .getDvrDataManager().getAvailableAndCanceledScheduledRecordings();
- List<ScheduledRecording> seriesScheduledRecordings = new ArrayList<>();
- if (mSeriesRecording == null) {
- return;
+ setPrograms(Collections.emptyList());
+ }
+
+ @Override
+ public void stop() {
+ super.stop();
+ }
+
+ /**
+ * Sets the programs to show.
+ */
+ public void setPrograms(List<Program> programs) {
+ if (programs == null) {
+ programs = Collections.emptyList();
}
- for (ScheduledRecording recording : recordings) {
- if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
- seriesScheduledRecordings.add(recording);
+ clear();
+ mPrograms.clear();
+ List<Program> sortedPrograms = new ArrayList<>(programs);
+ Collections.sort(sortedPrograms);
+ List<EpisodicProgramRow> rows = new ArrayList<>();
+ mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(),
+ null, sortedPrograms.size(), mSeriesRecording);
+ for (Program program : sortedPrograms) {
+ ScheduledRecording schedule =
+ mDataManager.getScheduledRecordingForProgramId(program.getId());
+ if (schedule != null && !willBeKept(schedule)) {
+ schedule = null;
}
+ rows.add(new EpisodicProgramRow(mInputId, program, schedule, mHeaderRow));
+ mPrograms.put(program.getId(), program);
}
- Collections.sort(seriesScheduledRecordings,
- ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR);
- int dayCountToLastRecording = 0;
- if (!seriesScheduledRecordings.isEmpty()) {
- long lastRecordingStartTimeMs = seriesScheduledRecordings
- .get(seriesScheduledRecordings.size() - 1).getStartTimeMs();
- dayCountToLastRecording = Utils.computeDateDifference(System.currentTimeMillis(),
- lastRecordingStartTimeMs) + 1;
+ mHeaderRow.setDescription(getDescription());
+ add(mHeaderRow);
+ for (EpisodicProgramRow row : rows) {
+ add(row);
}
- SchedulesHeaderRow headerRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(),
- getContext().getResources().getQuantityString(
- R.plurals.dvr_series_schedules_header_description, dayCountToLastRecording,
- dayCountToLastRecording), seriesScheduledRecordings.size(), mSeriesRecording);
- add(headerRow);
- for (ScheduledRecording recording : seriesScheduledRecordings) {
- add(new ScheduleRow(recording, headerRow));
+ sendNextUpdateMessage(System.currentTimeMillis());
+ }
+
+ private String getDescription() {
+ int conflicts = 0;
+ for (long programId : mPrograms.keySet()) {
+ if (mDvrManager.isConflicting(
+ mDataManager.getScheduledRecordingForProgramId(programId))) {
+ ++conflicts;
+ }
}
+ return conflicts == 0 ? null : getContext().getResources().getQuantityString(
+ R.plurals.dvr_series_schedules_header_description, conflicts, conflicts);
}
@Override
- public void stop() {
- SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG,
- "First row is not SchedulesHeaderRow");
- boolean cancelAll = size() > 0 && ((SeriesRecordingHeaderRow) get(0)).isCancelAllChecked();
- if (!cancelAll) {
- DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
- for (int i = 0; i < size(); i++) {
- if (get(i) instanceof ScheduleRow) {
- ScheduleRow scheduleRow = (ScheduleRow) get(i);
- if (scheduleRow.isRemoveScheduleChecked()) {
- dvrManager.removeScheduledRecording(scheduleRow.getRecording());
- }
- }
+ public long getId(int position) {
+ Object obj = get(position);
+ if (obj instanceof EpisodicProgramRow) {
+ return ((EpisodicProgramRow) obj).getProgram().getId();
+ }
+ if (obj instanceof SeriesRecordingHeaderRow) {
+ return 0;
+ }
+ return super.getId(position);
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording schedule) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule);
+ int index = findRowIndexByProgramId(schedule.getProgramId());
+ if (index != -1) {
+ EpisodicProgramRow row = (EpisodicProgramRow) get(index);
+ if (!row.isStartRecordingRequested()) {
+ row.setSchedule(schedule);
+ notifyArrayItemRangeChanged(index, 1);
}
}
}
@Override
- protected void addScheduleRow(ScheduledRecording recording) {
- if (recording != null && recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
- int index = 0;
- for (; index < size(); index++) {
- if (get(index) instanceof ScheduleRow) {
- ScheduleRow scheduleRow = (ScheduleRow) get(index);
- if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare(
- scheduleRow.getRecording(), recording) > 0) {
- break;
+ public void onScheduledRecordingRemoved(ScheduledRecording schedule) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule);
+ int index = findRowIndexByProgramId(schedule.getProgramId());
+ if (index != -1) {
+ EpisodicProgramRow row = (EpisodicProgramRow) get(index);
+ row.setSchedule(null);
+ notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) {
+ if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule);
+ int index = findRowIndexByProgramId(schedule.getProgramId());
+ if (index != -1) {
+ EpisodicProgramRow row = (EpisodicProgramRow) get(index);
+ if (conflictChange && isStartOrStopRequested()) {
+ // Delay the conflict update until it gets the response of the start/stop request.
+ // The purpose is to avoid the intermediate conflict change.
+ addPendingUpdate(row);
+ return;
+ }
+ if (row.isStopRecordingRequested()) {
+ // Wait until the recording is finished
+ if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ row.setStopRecordingRequested(false);
+ if (!isStartOrStopRequested()) {
+ executePendingUpdate();
}
+ row.setSchedule(null);
}
+ } else if (row.isStartRecordingRequested()) {
+ // When the start recording was requested, we give the highest priority. So it is
+ // guaranteed that the state will be changed from NOT_STARTED to the other state.
+ // Update the row with the next state not to show the intermediate state to avoid
+ // blinking.
+ if (schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ row.setStartRecordingRequested(false);
+ if (!isStartOrStopRequested()) {
+ executePendingUpdate();
+ }
+ row.setSchedule(schedule);
+ }
+ } else if (willBeKept(schedule)) {
+ row.setSchedule(schedule);
+ } else {
+ row.setSchedule(null);
}
- SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG,
- "First row is not SchedulesHeaderRow");
- if (index == 0) {
- index++;
- }
- SchedulesHeaderRow headerRow = (SchedulesHeaderRow) get(0);
- headerRow.setItemCount(headerRow.getItemCount() + 1);
- ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
- add(index, addedRow);
- updateHeaderRowDescription(headerRow);
+ notifyArrayItemRangeChanged(index, 1);
}
}
- @Override
- protected void removeScheduleRow(ScheduleRow scheduleRow) {
- if (scheduleRow != null) {
- remove(scheduleRow);
- SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
- // Changes the count information of header which the removed row belongs to.
- if (headerRow != null) {
- headerRow.setItemCount(headerRow.getItemCount() - 1);
- if (headerRow.getItemCount() == 0) {
- // TODO: Add a emtpy view.
- } else if (get(size() - 1) instanceof ScheduleRow) {
- updateHeaderRowDescription(headerRow);
+ public void onSeriesRecordingUpdated(SeriesRecording seriesRecording) {
+ if (seriesRecording.getId() == mSeriesRecording.getId()) {
+ mHeaderRow.setSeriesRecording(seriesRecording);
+ notifyArrayItemRangeChanged(0, 1);
+ }
+ }
+
+ private int findRowIndexByProgramId(long programId) {
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof EpisodicProgramRow) {
+ if (((EpisodicProgramRow) item).getProgram().getId() == programId) {
+ return i;
}
}
}
+ return -1;
}
@Override
- protected boolean willBeKept(ScheduledRecording recording) {
- return super.willBeKept(recording)
- || recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED;
+ public void notifyArrayItemRangeChanged(int positionStart, int itemCount) {
+ mHeaderRow.setDescription(getDescription());
+ super.notifyArrayItemRangeChanged(0, 1);
+ super.notifyArrayItemRangeChanged(positionStart, itemCount);
}
- private void updateHeaderRowDescription(SchedulesHeaderRow headerRow) {
- int nextDays = Utils.computeDateDifference(System.currentTimeMillis(),
- ((ScheduleRow) get(size() - 1)).getRecording().getStartTimeMs()) + 1;
- headerRow.setDescription(getContext().getResources()
- .getQuantityString(R.plurals.dvr_series_schedules_header_description,
- nextDays, nextDays));
- replace(indexOf(headerRow), headerRow);
+ @Override
+ protected void handleUpdateRow(long currentTimeMs) {
+ for (Iterator<Program> iter = mPrograms.values().iterator(); iter.hasNext(); ) {
+ Program program = iter.next();
+ if (program.getEndTimeUtcMillis() <= currentTimeMs) {
+ // Remove the old program.
+ removeItems(findRowIndexByProgramId(program.getId()), 1);
+ iter.remove();
+ } else if (program.getStartTimeUtcMillis() < currentTimeMs) {
+ // Change the button "START RECORDING"
+ notifyItemRangeChanged(findRowIndexByProgramId(program.getId()), 1);
+ }
+ }
+ }
+
+ /**
+ * Should take the current time argument which is the time when the programs are checked in
+ * handler.
+ */
+ @Override
+ protected long getNextTimerMs(long currentTimeMs) {
+ long earliest = Long.MAX_VALUE;
+ for (Program program : mPrograms.values()) {
+ if (earliest > program.getStartTimeUtcMillis()
+ && program.getStartTimeUtcMillis() >= currentTimeMs) {
+ // Need the button from "CREATE SCHEDULE" to "START RECORDING"
+ earliest = program.getStartTimeUtcMillis();
+ } else if (earliest > program.getEndTimeUtcMillis()) {
+ // Need to remove the row.
+ earliest = program.getEndTimeUtcMillis();
+ }
+ }
+ return earliest;
}
}
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
index 4f31528c..5d88579a 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
@@ -21,16 +21,16 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
import com.android.tv.util.Utils;
/**
* A RowPresenter for series schedule row.
*/
public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
- private boolean mIsCancelAll;
+ private static final String TAG = "SeriesRowPresenter";
+
private boolean mLtr;
public SeriesScheduleRowPresenter(Context context) {
@@ -40,8 +40,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
}
public static class SeriesScheduleRowViewHolder extends ScheduleRowViewHolder {
- public SeriesScheduleRowViewHolder(View view) {
- super(view);
+ public SeriesScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) {
+ super(view, presenter);
ViewGroup.LayoutParams lp = getTimeView().getLayoutParams();
lp.width = view.getResources().getDimensionPixelSize(
R.dimen.dvr_series_schedules_item_time_width);
@@ -51,37 +51,29 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
@Override
protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) {
- return new SeriesScheduleRowViewHolder(view);
+ return new SeriesScheduleRowViewHolder(view, this);
}
@Override
- protected String onGetRecordingTimeText(ScheduledRecording recording) {
- return Utils.getDurationString(getContext(),
- recording.getStartTimeMs(), recording.getEndTimeMs(), false, true, true, 0);
+ protected String onGetRecordingTimeText(ScheduleRow row) {
+ return Utils.getDurationString(getContext(), row.getStartTimeMs(), row.getEndTimeMs(),
+ false, true, true, 0);
}
@Override
- protected String onGetProgramInfoText(ScheduledRecording recording) {
- if (recording != null) {
- return recording.getEpisodeDisplayTitle(getContext());
- }
- return null;
+ protected String onGetProgramInfoText(ScheduleRow row) {
+ return row.getEpisodeDisplayTitle(getContext());
}
@Override
- protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder,
- ScheduleRow scheduleRow) {
- mIsCancelAll = ((SeriesRecordingHeaderRow) scheduleRow.getHeaderRow()).isCancelAllChecked();
- boolean isConflicting = getConflicts().contains(scheduleRow.getRecording());
- if (mIsCancelAll || isConflicting || scheduleRow.isRemoveScheduleChecked()) {
- viewHolder.greyOutInfo();
- } else {
- viewHolder.whiteBackInfo();
- }
- if (!mIsCancelAll && isConflicting) {
+ protected void onBindRowViewHolder(ViewHolder vh, Object item) {
+ super.onBindRowViewHolder(vh, item);
+ SeriesScheduleRowViewHolder viewHolder = (SeriesScheduleRowViewHolder) vh;
+ EpisodicProgramRow row = (EpisodicProgramRow) item;
+ if (getDvrManager().isConflicting(row.getSchedule())) {
viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext()
.getResources().getDimensionPixelOffset(
- R.dimen.dvr_schedules_warning_icon_padding));
+ R.dimen.dvr_schedules_warning_icon_padding));
if (mLtr) {
viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
@@ -92,26 +84,60 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
} else {
viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
- if (mIsCancelAll) {
- viewHolder.getInfoContainer().setClickable(false);
- viewHolder.getDeleteActionContainer().setVisibility(View.GONE);
+ }
+
+ @Override
+ protected void onInfoClicked(ScheduleRow row) {
+ if (row.getSchedule() != null) {
+ DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule());
}
}
@Override
- protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) {
- ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
- if (!mIsCancelAll) {
- if (selected) {
- viewHolder.getDeleteActionContainer().setVisibility(View.VISIBLE);
+ protected void onStartRecording(ScheduleRow row) {
+ SoftPreconditions.checkState(row.getSchedule() == null, TAG,
+ "Start request with the existing schedule: " + row);
+ row.setStartRecordingRequested(true);
+ getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram());
+ }
+
+ @Override
+ protected void onStopRecording(ScheduleRow row) {
+ SoftPreconditions.checkState(row.getSchedule() != null, TAG,
+ "Stop request with the null schedule: " + row);
+ row.setStopRecordingRequested(true);
+ getDvrManager().stopRecording(row.getSchedule());
+ }
+
+ @Override
+ protected void onCreateSchedule(ScheduleRow row) {
+ if (row.getSchedule() == null) {
+ getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram());
+ } else {
+ super.onCreateSchedule(row);
+ }
+ }
+
+ @Override
+ @ScheduleRowAction
+ protected int[] getAvailableActions(ScheduleRow row) {
+ if (row.getSchedule() == null) {
+ if (row.isOnAir()) {
+ return new int[] {ACTION_START_RECORDING};
} else {
- viewHolder.getDeleteActionContainer().setVisibility(View.GONE);
+ return new int[] {ACTION_CREATE_SCHEDULE};
}
}
+ return super.getAvailableActions(row);
+ }
+
+ @Override
+ protected boolean canResolveConflict() {
+ return false;
}
@Override
- protected void onInfoClicked(ScheduleRow scheduleRow) {
- DvrUiHelper.startSchedulesActivity(getContext(), scheduleRow.getRecording());
+ protected boolean shouldKeepScheduleAfterRemoving() {
+ return true;
}
}
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index a6ac4375..120b3dba 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -22,7 +22,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
+import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Point;
@@ -42,6 +42,7 @@ import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
import com.android.tv.ChannelTuner;
import com.android.tv.Features;
@@ -56,6 +57,7 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
+import com.android.tv.ui.ViewUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -90,6 +92,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
private final MainActivity mActivity;
private final ProgramManager mProgramManager;
+ private final AccessibilityManager mAccessibilityManager;
private final ChannelTuner mChannelTuner;
private final Tracker mTracker;
private final DurationTimer mVisibleDuration = new DurationTimer();
@@ -374,7 +377,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mProgramTableFadeInAnimator.setTarget(mTable);
mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity);
- mShowGuidePartial = mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
+ mAccessibilityManager =
+ (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ mShowGuidePartial = mAccessibilityManager.isEnabled()
+ || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
}
private void updateGuidePosition() {
@@ -606,7 +612,9 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
private void startFull() {
- if (isFull()) {
+ if (isFull() || mAccessibilityManager.isEnabled()) {
+ // If accessibility service is enabled, focus cannot be moved to side panel due to it's
+ // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
return;
}
mShowGuidePartial = false;
@@ -743,7 +751,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
View detailView = row.findViewById(R.id.detail);
detailView.findViewById(R.id.detail_content_full).setAlpha(1);
detailView.findViewById(R.id.detail_content_full).setTranslationY(0);
- setLayoutHeight(detailView, mDetailHeight);
+ ViewUtils.setLayoutHeight(detailView, mDetailHeight);
detailView.setVisibility(View.VISIBLE);
final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
@@ -785,8 +793,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
fadeOutAnimator.setDuration(mAnimationDuration);
fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent));
- Animator collapseAnimator =
- createHeightAnimator(outDetail, getLayoutHeight(outDetail), 0);
+ Animator collapseAnimator = ViewUtils
+ .createHeightAnimator(outDetail, ViewUtils.getLayoutHeight(outDetail), 0);
collapseAnimator.setStartDelay(mAnimationDuration);
collapseAnimator.setDuration(mTableFadeAnimDuration);
collapseAnimator.addListener(new AnimatorListenerAdapter() {
@@ -817,7 +825,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
if (inDetail != null) {
final View inDetailContent = inDetail.findViewById(R.id.detail_content_full);
- Animator expandAnimator = createHeightAnimator(inDetail, 0, mDetailHeight);
+ Animator expandAnimator = ViewUtils.createHeightAnimator(inDetail, 0, mDetailHeight);
expandAnimator.setStartDelay(mAnimationDuration);
expandAnimator.setDuration(mTableFadeAnimDuration);
expandAnimator.addListener(new AnimatorListenerAdapter() {
@@ -832,17 +840,15 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
inDetailContent.setAlpha(0);
}
});
-
Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent,
PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
direction * -mDetailPadding, 0f));
- fadeInAnimator.setStartDelay(mAnimationDuration + mTableFadeAnimDuration);
fadeInAnimator.setDuration(mAnimationDuration);
fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent));
AnimatorSet inAnimator = new AnimatorSet();
- inAnimator.playTogether(expandAnimator, fadeInAnimator);
+ inAnimator.playSequentially(expandAnimator, fadeInAnimator);
inAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
@@ -854,41 +860,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
}
- private Animator createHeightAnimator(
- final View target, int initialHeight, int targetHeight) {
- ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- int value = (Integer) animation.getAnimatedValue();
- if (value == 0) {
- if (target.getVisibility() != View.GONE) {
- target.setVisibility(View.GONE);
- }
- } else {
- if (target.getVisibility() != View.VISIBLE) {
- target.setVisibility(View.VISIBLE);
- }
- setLayoutHeight(target, value);
- }
- }
- });
- return animator;
- }
-
- private int getLayoutHeight(View view) {
- LayoutParams layoutParams = view.getLayoutParams();
- return layoutParams.height;
- }
-
- private void setLayoutHeight(View view, int height) {
- LayoutParams layoutParams = view.getLayoutParams();
- if (height != layoutParams.height) {
- layoutParams.height = height;
- view.setLayoutParams(layoutParams);
- }
- }
-
private class GlobalFocusChangeListener implements
ViewTreeObserver.OnGlobalFocusChangeListener {
private static final int UNKNOWN = 0;
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index f638e830..4c7a4404 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -47,9 +47,9 @@ import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.guide.ProgramManager.TableEntry;
+import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
-import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.TimeUnit;
@@ -73,8 +73,6 @@ public class ProgramItemView extends TextView {
private static TextAppearanceSpan sEpisodeTitleStyle;
private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle;
- private static WeakReference<Toast> sToast;
-
private DvrManager mDvrManager;
private TableEntry mTableEntry;
private int mMaxWidthForRipple;
@@ -96,10 +94,9 @@ public class ProgramItemView extends TextView {
ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext());
Tracker tracker = singletons.getTracker();
tracker.sendEpgItemClicked();
+ final MainActivity tvActivity = (MainActivity) view.getContext();
+ final Channel channel = tvActivity.getChannelDataManager().getChannel(entry.channelId);
if (entry.isCurrentProgram()) {
- final MainActivity tvActivity = (MainActivity) view.getContext();
- final Channel channel = tvActivity.getChannelDataManager()
- .getChannel(entry.channelId);
view.postDelayed(new Runnable() {
@Override
public void run() {
@@ -114,42 +111,25 @@ public class ProgramItemView extends TextView {
if (entry.entryStartUtcMillis > System.currentTimeMillis()
&& dvrManager.isProgramRecordable(entry.program)) {
if (entry.scheduledRecording == null) {
- if (DvrUiHelper.handleCreateSchedule((MainActivity) view.getContext(),
- entry.program)) {
+ if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity,
+ channel.getInputId())
+ && DvrUiHelper.handleCreateSchedule(tvActivity, entry.program)) {
String msg = view.getContext().getString(
R.string.dvr_msg_program_scheduled, entry.program.getTitle());
- showToast(view.getContext(), msg);
+ ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT);
}
- } else if (entry.scheduledRecording.getState()
- == ScheduledRecording.STATE_RECORDING_CANCELED) {
- // TODO: replace the toast with a dialog.
- String msg = view.getResources().getString(
- R.string.dvr_msg_program_scheduled, entry.program.getTitle());
- showToast(view.getContext(), msg);
- dvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(entry
- .scheduledRecording).setState(ScheduledRecording
- .STATE_RECORDING_NOT_STARTED).build());
} else {
dvrManager.removeScheduledRecording(entry.scheduledRecording);
String msg = view.getResources().getString(
R.string.dvr_schedules_deletion_info, entry.program.getTitle());
- showToast(view.getContext(), msg);
+ ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT);
}
} else {
- showToast(view.getContext(), view.getResources()
- .getString(R.string.dvr_msg_cannot_record_program));
+ ToastUtils.show(view.getContext(), view.getResources()
+ .getString(R.string.dvr_msg_cannot_record_program), Toast.LENGTH_SHORT);
}
}
}
-
- private void showToast(Context context, String msg) {
- if (sToast != null && sToast.get() != null) {
- sToast.get().cancel();
- }
- Toast toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
- toast.show();
- sToast = new WeakReference<>(toast);
- }
};
private static final View.OnFocusChangeListener ON_FOCUS_CHANGED =
@@ -322,7 +302,7 @@ public class ProgramItemView extends TextView {
int iconResId = 0;
if (mTableEntry.scheduledRecording != null) {
if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) {
- iconResId = R.drawable.ic_warning_white_36dp;
+ iconResId = R.drawable.ic_warning_white_18dp;
} else {
switch (mTableEntry.scheduledRecording.getState()) {
case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index 489cc5ef..e3d919df 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -204,17 +204,17 @@ public class ProgramManager {
@Override
public void onLoadFinished() {
mChannelDataLoaded = true;
- updateChannels(true, false);
+ updateChannels(false);
}
@Override
public void onChannelListUpdated() {
- updateChannels(true, false);
+ updateChannels(false);
}
@Override
public void onChannelBrowsableChanged() {
- updateChannels(true, false);
+ updateChannels(false);
}
};
@@ -222,7 +222,7 @@ public class ProgramManager {
new ProgramDataManager.Listener() {
@Override
public void onProgramUpdated() {
- updateTableEntries(true, true);
+ updateTableEntries(true);
}
};
@@ -434,18 +434,16 @@ public class ProgramManager {
// Note that This can be happens only if program guide isn't shown
// because an user has to select channels as browsable through UI.
- private void updateChannels(boolean notify, boolean clearPreviousTableEntries) {
+ private void updateChannels(boolean clearPreviousTableEntries) {
if (DEBUG) Log.d(TAG, "updateChannels");
mChannels = mChannelDataManager.getBrowsableChannelList();
mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
mFilteredChannels = mChannels;
- if (notify) {
- notifyChannelsUpdated();
- }
- updateTableEntries(notify, clearPreviousTableEntries);
+ notifyChannelsUpdated();
+ updateTableEntries(clearPreviousTableEntries);
}
- private void updateTableEntries(boolean notify, boolean clear) {
+ private void updateTableEntries(boolean clear) {
if (clear) {
mChannelIdEntriesMap.clear();
}
@@ -494,9 +492,7 @@ public class ProgramManager {
}
}
- if (notify) {
- notifyTableEntriesUpdated();
- }
+ notifyTableEntriesUpdated();
buildGenreFilters();
}
@@ -579,7 +575,7 @@ public class ProgramManager {
}
mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
- updateChannels(true, true);
+ updateChannels(true);
setTimeRange(startUtcMillis, endUtcMillis);
}
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 5c4236a6..2c98ab2d 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -22,8 +22,7 @@ import android.support.v7.widget.LinearLayoutManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
-import android.view.ViewParent;
-import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import com.android.tv.data.Channel;
import com.android.tv.guide.ProgramManager.TableEntry;
@@ -45,25 +44,26 @@ public class ProgramRow extends TimelineGridView {
interface ChildFocusListener {
/**
- * Is called after focus is moved. Only children to {@code ProgramRow} will be passed.
+ * Is called after focus is moved. It used {@link ChildFocusListener#isChild} to decide if
+ * old and new focuses are listener's children.
* See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}.
*/
void onChildFocus(View oldFocus, View newFocus);
}
- private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
- new ViewTreeObserver.OnGlobalFocusChangeListener() {
- @Override
- public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- updateCurrentFocus(oldFocus, newFocus);
- }
- };
-
/**
* Used only for debugging.
*/
private Channel mChannel;
+ private final OnGlobalLayoutListener mLayoutListener = new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ updateChildVisibleArea();
+ }
+ };
+
public ProgramRow(Context context) {
this(context, null);
}
@@ -94,19 +94,15 @@ public class ProgramRow extends TimelineGridView {
@Override
public void onScrolled(int dx, int dy) {
+ // Remove callback to prevent updateChildVisibleArea being called twice.
+ getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener);
super.onScrolled(dx, dy);
- int childCount = getChildCount();
if (DEBUG) {
Log.d(TAG, "onScrolled by " + dx);
- Log.d(TAG, "channelId=" + mChannel.getId() + ", childCount=" + childCount);
+ Log.d(TAG, "channelId=" + mChannel.getId() + ", childCount=" + getChildCount());
Log.d(TAG, "ProgramRow {" + Utils.toRectString(this) + "}");
}
- for (int i = 0; i < childCount; ++i) {
- ProgramItemView child = (ProgramItemView) getChildAt(i);
- if (getLeft() <= child.getRight() && child.getLeft() <= getRight()) {
- child.updateVisibleArea();
- }
- }
+ updateChildVisibleArea();
}
/**
@@ -117,29 +113,9 @@ public class ProgramRow extends TimelineGridView {
if (currentProgram == null) {
currentProgram = getChildAt(0);
}
- updateCurrentFocus(null, currentProgram);
- }
-
- private void updateCurrentFocus(View oldFocus, View newFocus) {
- if (mChildFocusListener == null) {
- return;
- }
-
- mChildFocusListener.onChildFocus(isChild(oldFocus) ? oldFocus : null,
- isChild(newFocus) ? newFocus : null);
- }
-
- private boolean isChild(View view) {
- if (view == null) {
- return false;
- }
-
- for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
- if (p == this) {
- return true;
- }
+ if (mChildFocusListener != null) {
+ mChildFocusListener.onChildFocus(null, currentProgram);
}
- return false;
}
// Call this API after RTL is resolved. (i.e. View is measured.)
@@ -216,23 +192,21 @@ public class ProgramRow extends TimelineGridView {
}
@Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- getViewTreeObserver().addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- getViewTreeObserver().removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
- }
-
- @Override
public void onChildDetachedFromWindow(View child) {
if (child.hasFocus()) {
// Focused view can be detached only if it's updated.
TableEntry entry = ((ProgramItemView) child).getTableEntry();
- if (entry.isCurrentProgram()) {
+ if (entry.program == null) {
+ // The focus is lost due to information loaded. Requests focus immediately.
+ // (Because this entry is detached after real entries attached, we can't take
+ // the below approach to resume focus on entry being attached.)
+ post(new Runnable() {
+ @Override
+ public void run() {
+ requestFocus();
+ }
+ });
+ } else if (entry.isCurrentProgram()) {
if (DEBUG) Log.d(TAG, "Keep focus to the current program");
// Current program is visible in the guide.
// Updated entries including current program's will be attached again soon
@@ -250,13 +224,13 @@ public class ProgramRow extends TimelineGridView {
if (mKeepFocusToCurrentProgram) {
TableEntry entry = ((ProgramItemView) child).getTableEntry();
if (entry.isCurrentProgram()) {
+ mKeepFocusToCurrentProgram = false;
post(new Runnable() {
@Override
public void run() {
requestFocus();
}
});
- mKeepFocusToCurrentProgram = false;
}
}
}
@@ -324,6 +298,22 @@ public class ProgramRow extends TimelineGridView {
mProgramManager.getStartTime(), entry.entryStartUtcMillis) - scrollOffset;
((LinearLayoutManager) getLayoutManager())
.scrollToPositionWithOffset(position, offset);
+ // Workaround to b/31598505. When a program's duration is too long,
+ // RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset().
+ // Therefore we have to update children's visible areas by ourselves in theis case.
+ // Since scrollToPositionWithOffset() will call requestLayout(), we can listen to this
+ // behavior to ensure program items' visible areas are correctly updated after layouts
+ // are adjusted, i.e., scrolling is over.
+ getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener);
+ }
+ }
+
+ private void updateChildVisibleArea() {
+ for (int i = 0; i < getChildCount(); ++i) {
+ ProgramItemView child = (ProgramItemView) getChildAt(i);
+ if (getLeft() < child.getRight() && child.getLeft() < getRight()) {
+ child.updateVisibleArea();
+ }
}
}
}
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index fd561992..e4a67972 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -17,6 +17,7 @@
package com.android.tv.guide;
import static com.android.tv.util.ImageLoader.ImageLoaderCallback;
+
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
@@ -42,6 +43,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -50,10 +53,10 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
-import com.android.tv.data.Program.CriticScore;
import com.android.tv.data.Program;
-import com.android.tv.dvr.DvrManager;
+import com.android.tv.data.Program.CriticScore;
import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener;
import com.android.tv.parental.ParentalControlSettings;
@@ -80,6 +83,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
private final DvrManager mDvrManager;
private final DvrDataManager mDvrDataManager;
private final ProgramManager mProgramManager;
+ private final AccessibilityManager mAccessibilityManager;
private final ProgramGuide mProgramGuide;
private final Handler mHandler = new Handler();
private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>();
@@ -103,6 +107,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
private final String mProgramRecordableText;
private final String mRecordingScheduledText;
private final String mRecordingConflictText;
+ private final String mRecordingFailedText;
private final String mRecordingInProgressText;
private final int mDvrPaddingStartWithTrack;
private final int mDvrPaddingStartWithOutTrack;
@@ -110,6 +115,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
public ProgramTableAdapter(Context context, ProgramManager programManager,
ProgramGuide programGuide) {
mContext = context;
+ mAccessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper();
if (CommonFeatures.DVR.isEnabled(context)) {
mDvrManager = TvApplication.getSingletons(context).getDvrManager();
@@ -149,6 +156,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mProgramRecordableText = res.getString(R.string.dvr_epg_program_recordable);
mRecordingScheduledText = res.getString(R.string.dvr_epg_program_recording_scheduled);
mRecordingConflictText = res.getString(R.string.dvr_epg_program_recording_conflict);
+ mRecordingFailedText = res.getString(R.string.dvr_epg_program_recording_failed);
mRecordingInProgressText = res.getString(R.string.dvr_epg_program_recording_in_progress);
mDvrPaddingStartWithTrack = res.getDimensionPixelOffset(
R.dimen.program_guide_table_detail_dvr_margin_start);
@@ -162,7 +170,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize,
episodeTitleColor, null);
- mCriticScoreViews = new ArrayList<LinearLayout>();
+ mCriticScoreViews = new ArrayList<>();
mRecycledViewPool = new RecycledViewPool();
mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item,
context.getResources().getInteger(
@@ -170,12 +178,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mProgramManager.addListener(new ProgramManager.ListenerAdapter() {
@Override
public void onChannelsUpdated() {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- update();
- }
- });
+ update();
}
});
update();
@@ -238,6 +241,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
notifyItemChanged(channelIndex, true);
}
+ @Override
+ public void onViewAttachedToWindow(ProgramRowHolder holder) {
+ holder.onAttachedToWindow();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(ProgramRowHolder holder) {
+ holder.onDetachedFromWindow();
+ }
+
// TODO: make it static
public class ProgramRowHolder extends RecyclerView.ViewHolder
implements ProgramRow.ChildFocusListener {
@@ -265,6 +278,15 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
};
+ private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
+ new ViewTreeObserver.OnGlobalFocusChangeListener() {
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ onChildFocus(isChild(oldFocus) ? oldFocus : null,
+ isChild(newFocus) ? newFocus : null);
+ }
+ };
+
// Members of Program Details
private final ViewGroup mDetailView;
private final ImageView mImageView;
@@ -317,6 +339,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo);
mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block);
mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo);
+ mDetailView.setFocusable(mAccessibilityManager.isEnabled());
+ mChannelHeaderView.setFocusable(mAccessibilityManager.isEnabled());
+ mAccessibilityManager.addAccessibilityStateChangeListener(
+ new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enable) {
+ mDetailView.setFocusable(enable);
+ mChannelHeaderView.setFocusable(enable);
+ }
+ });
}
public void onBind(int position) {
@@ -384,12 +416,32 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
}
+ public boolean isChild(View view) {
+ if (view == null) {
+ return false;
+ }
+ for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
+ if (p == mContainer) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public void onChildFocus(View oldFocus, View newFocus) {
if (newFocus == null) {
return;
}
- mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry();
+ // When the accessibility service is enabled, focus might be put on channel's header or
+ // detail view, besides program items.
+ if (newFocus == mChannelHeaderView) {
+ mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry();
+ } else if (newFocus == mDetailView) {
+ return;
+ } else {
+ mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry();
+ }
if (oldFocus == null) {
updateDetailView();
return;
@@ -456,7 +508,21 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
});
}
+ private void onAttachedToWindow() {
+ mContainer.getViewTreeObserver()
+ .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ }
+
+ private void onDetachedFromWindow() {
+ mContainer.getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ }
+
private void updateDetailView() {
+ if (mSelectedEntry == null) {
+ // The view holder is never on focus before.
+ return;
+ }
if (DEBUG) Log.d(TAG, "updateDetailView");
mCriticScoresLayout.removeAllViews();
if (Program.isValid(mSelectedEntry.program)) {
@@ -508,7 +574,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
int iconResId = 0;
if (scheduledRecording != null) {
if (mDvrManager.isConflicting(scheduledRecording)) {
- iconResId = R.drawable.ic_warning_white_24dp;
+ iconResId = R.drawable.ic_warning_white_12dp;
statusText = mRecordingConflictText;
} else {
switch (scheduledRecording.getState()) {
@@ -521,8 +587,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
statusText = mRecordingScheduledText;
break;
case ScheduledRecording.STATE_RECORDING_FAILED:
- iconResId = R.drawable.ic_warning_white_24dp;
- statusText = mRecordingConflictText;
+ iconResId = R.drawable.ic_warning_white_12dp;
+ statusText = mRecordingFailedText;
break;
default:
iconResId = 0;
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index 2d72b06f..54892cac 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -69,11 +69,13 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV
mStateView.setText(action.getActionDescription(getContext()));
if (action.isEnabled()) {
setEnabled(true);
+ setFocusable(true);
mIconView.setAlpha(OPACITY_ENABLED);
mLabelView.setAlpha(OPACITY_ENABLED);
mStateView.setAlpha(OPACITY_ENABLED);
} else {
setEnabled(false);
+ setFocusable(false);
mIconView.setAlpha(OPACITY_DISABLED);
mLabelView.setAlpha(OPACITY_DISABLED);
mStateView.setAlpha(OPACITY_DISABLED);
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index a71b1a61..bfb5e3f1 100644
--- a/src/com/android/tv/menu/AppLinkCardView.java
+++ b/src/com/android/tv/menu/AppLinkCardView.java
@@ -30,7 +30,6 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
@@ -48,10 +47,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
- private final float mCardHeight;
- private final float mExtendedCardHeight;
- private final float mTextViewHeight;
- private final float mExtendedTextViewCardHeight;
private final int mCardImageWidth;
private final int mCardImageHeight;
private final int mIconWidth;
@@ -62,12 +57,9 @@ public class AppLinkCardView extends BaseCardView<Channel> {
private ImageView mImageView;
private View mGradientView;
private TextView mAppInfoView;
- private TextView mMetaViewFocused;
- private TextView mMetaViewUnfocused;
private View mMetaViewHolder;
private Channel mChannel;
private Intent mIntent;
- private boolean mExtendViewOnFocus;
private final PackageManager mPackageManager;
private final TvInputManagerHelper mTvInputManagerHelper;
@@ -84,18 +76,11 @@ public class AppLinkCardView extends BaseCardView<Channel> {
mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width);
mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.card_image_layout_height);
- mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height);
- mExtendedCardHeight = getResources().getDimensionPixelOffset(
- R.dimen.card_layout_height_extended);
mIconWidth = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_width);
mIconHeight = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_height);
mIconPadding = getResources().getDimensionPixelOffset(R.dimen.app_link_card_icon_padding);
mPackageManager = context.getPackageManager();
mTvInputManagerHelper = ((MainActivity) context).getTvInputManagerHelper();
- mTextViewHeight = getResources().getDimensionPixelSize(
- R.dimen.card_meta_layout_height);
- mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset(
- R.dimen.card_meta_layout_height_extended);
mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null);
}
@@ -119,7 +104,7 @@ public class AppLinkCardView extends BaseCardView<Channel> {
switch (linkType) {
case Channel.APP_LINK_TYPE_CHANNEL:
- setMetaViewText(mChannel.getAppLinkText());
+ setText(mChannel.getAppLinkText());
mAppInfoView.setVisibility(VISIBLE);
mGradientView.setVisibility(VISIBLE);
mAppInfoView.setCompoundDrawablePadding(mIconPadding);
@@ -137,7 +122,7 @@ public class AppLinkCardView extends BaseCardView<Channel> {
}
break;
case Channel.APP_LINK_TYPE_APP:
- setMetaViewText(getContext().getString(
+ setText(getContext().getString(
R.string.channels_item_app_link_app_launcher,
mPackageManager.getApplicationLabel(appInfo)));
mAppInfoView.setVisibility(GONE);
@@ -163,17 +148,8 @@ public class AppLinkCardView extends BaseCardView<Channel> {
} else {
setCardImageWithBanner(appInfo);
}
-
- mMetaViewFocused.measure(MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- mExtendViewOnFocus = mMetaViewFocused.getLineCount() > 1;
- if (mExtendViewOnFocus) {
- setMetaViewFocusedAlpha(selected ? 1f : 0f);
- } else {
- setMetaViewFocusedAlpha(1f);
- }
-
- // Call super.onBind() at the end in order to make getCardHeight() return a proper value.
+ // Call super.onBind() at the end intentionally. In order to correctly handle extension of
+ // text view, text should be set before calling super.onBind.
super.onBind(channel, selected);
}
@@ -227,32 +203,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
mGradientView = findViewById(R.id.image_gradient);
mAppInfoView = (TextView) findViewById(R.id.app_info);
mMetaViewHolder = findViewById(R.id.app_link_text_holder);
- mMetaViewFocused = (TextView) findViewById(R.id.app_link_text_focused);
- mMetaViewUnfocused = (TextView) findViewById(R.id.app_link_text_unfocused);
- }
-
- @Override
- protected void onFocusAnimationStart(boolean selected) {
- if (mExtendViewOnFocus) {
- setMetaViewFocusedAlpha(selected ? 1f : 0f);
- }
- }
-
- @Override
- protected void onSetFocusAnimatedValue(float animatedValue) {
- super.onSetFocusAnimatedValue(animatedValue);
- if (mExtendViewOnFocus) {
- ViewGroup.LayoutParams params = mMetaViewUnfocused.getLayoutParams();
- params.height = Math.round(mTextViewHeight
- + (mExtendedTextViewCardHeight - mTextViewHeight) * animatedValue);
- setMetaViewLayoutParams(params);
- setMetaViewFocusedAlpha(animatedValue);
- }
- }
-
- @Override
- protected float getCardHeight() {
- return (mExtendViewOnFocus && isFocused()) ? mExtendedCardHeight : mCardHeight;
}
// Try to set the card image with following order:
@@ -305,19 +255,4 @@ public class AppLinkCardView extends BaseCardView<Channel> {
}
});
}
-
- private void setMetaViewLayoutParams(ViewGroup.LayoutParams params) {
- mMetaViewFocused.setLayoutParams(params);
- mMetaViewUnfocused.setLayoutParams(params);
- }
-
- private void setMetaViewText(String text) {
- mMetaViewFocused.setText(text);
- mMetaViewUnfocused.setText(text);
- }
-
- private void setMetaViewFocusedAlpha(float focusedAlpha) {
- mMetaViewFocused.setAlpha(focusedAlpha);
- mMetaViewUnfocused.setAlpha(1f - focusedAlpha);
- }
}
diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java
index b4500dd1..c6a34a5d 100644
--- a/src/com/android/tv/menu/BaseCardView.java
+++ b/src/com/android/tv/menu/BaseCardView.java
@@ -21,10 +21,13 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.LinearLayout;
+import android.widget.TextView;
import com.android.tv.R;
@@ -44,6 +47,16 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
private final float mVerticalCardMargin;
private final float mCardCornerRadius;
private float mFocusAnimatedValue;
+ private boolean mExtendViewOnFocus;
+ private final float mExtendedCardHeight;
+ private final float mTextViewHeight;
+ private final float mExtendedTextViewHeight;
+ @Nullable
+ private TextView mTextView;
+ @Nullable
+ private TextView mTextViewFocused;
+ private final int mCardImageWidth;
+ private final float mCardHeight;
public BaseCardView(Context context) {
this(context, null);
@@ -72,16 +85,42 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius);
}
});
+ mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width);
+ mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height);
+ mExtendedCardHeight = getResources().getDimensionPixelSize(
+ R.dimen.card_layout_height_extended);
+ mTextViewHeight = getResources().getDimensionPixelSize(R.dimen.card_meta_layout_height);
+ mExtendedTextViewHeight = getResources().getDimensionPixelOffset(
+ R.dimen.card_meta_layout_height_extended);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mTextView = (TextView) findViewById(R.id.card_text);
+ mTextViewFocused = (TextView) findViewById(R.id.card_text_focused);
}
/**
* Called when the view is displayed.
+ *
+ * Before onBind is called, this view's text should be set to determine if it'll be extended
+ * or not in focus state.
*/
@Override
public void onBind(T item, boolean selected) {
- // Note that getCardHeight() will be called by setFocusAnimatedValue().
- // Therefore, be sure that getCardHeight() has a proper value before this method is called.
- setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
+ if (mTextView != null && mTextViewFocused != null) {
+ mTextViewFocused.measure(
+ MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1;
+ if (mExtendViewOnFocus) {
+ setTextViewFocusedAlpha(selected ? 1f : 0f);
+ } else {
+ setTextViewFocusedAlpha(1f);
+ }
+ }
+ setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
}
@Override
@@ -108,10 +147,48 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
}
/**
+ * Sets text of this card view.
+ */
+ public void setText(int resId) {
+ if (mTextViewFocused != null) {
+ mTextViewFocused.setText(resId);
+ }
+ if (mTextView != null) {
+ mTextView.setText(resId);
+ }
+ }
+
+ /**
+ * Sets text of this card view.
+ */
+ public void setText(String text) {
+ if (mTextViewFocused != null) {
+ mTextViewFocused.setText(text);
+ }
+ if (mTextView != null) {
+ mTextView.setText(text);
+ }
+ }
+
+ /**
+ * Enables or disables text view of this card view.
+ */
+ public void setTextViewEnabled(boolean enabled) {
+ if (mTextViewFocused != null) {
+ mTextViewFocused.setEnabled(enabled);
+ }
+ if (mTextView != null) {
+ mTextView.setEnabled(enabled);
+ }
+ }
+
+ /**
* Called when the focus animation started.
*/
protected void onFocusAnimationStart(boolean selected) {
- // do nothing.
+ if (mExtendViewOnFocus) {
+ setTextViewFocusedAlpha(selected ? 1f : 0f);
+ }
}
/**
@@ -126,10 +203,19 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
* between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}.
*/
protected void onSetFocusAnimatedValue(float animatedValue) {
- float scale = 1f + (mVerticalCardMargin / getCardHeight()) * animatedValue;
+ float cardViewHeight = (mExtendViewOnFocus && isFocused())
+ ? mExtendedCardHeight : mCardHeight;
+ float scale = 1f + (mVerticalCardMargin / cardViewHeight) * animatedValue;
setScaleX(scale);
setScaleY(scale);
setTranslationZ(mFocusTranslationZ * animatedValue);
+ if (mExtendViewOnFocus) {
+ ViewGroup.LayoutParams params = mTextView.getLayoutParams();
+ params.height = Math.round(mTextViewHeight
+ + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue);
+ setTextViewLayoutParams(params);
+ setTextViewFocusedAlpha(animatedValue);
+ }
}
private void setFocusAnimatedValue(float animatedValue) {
@@ -171,8 +257,13 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
}
}
- /**
- * The implementation should return the height of the card.
- */
- protected abstract float getCardHeight();
+ private void setTextViewLayoutParams(ViewGroup.LayoutParams params) {
+ mTextViewFocused.setLayoutParams(params);
+ mTextView.setLayoutParams(params);
+ }
+
+ private void setTextViewFocusedAlpha(float focusedAlpha) {
+ mTextViewFocused.setAlpha(focusedAlpha);
+ mTextView.setAlpha(1f - focusedAlpha);
+ }
}
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index 4e29a5a9..1c8015a6 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -23,7 +23,6 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -42,10 +41,6 @@ public class ChannelCardView extends BaseCardView<Channel> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
- private final float mCardHeight;
- private final float mExtendedCardHeight;
- private final float mProgramNameViewHeight;
- private final float mExtendedTextViewCardHeight;
private final int mCardImageWidth;
private final int mCardImageHeight;
@@ -53,11 +48,8 @@ public class ChannelCardView extends BaseCardView<Channel> {
private View mGradientView;
private TextView mChannelNumberNameView;
private ProgressBar mProgressBar;
- private TextView mMetaViewFocused;
- private TextView mMetaViewUnfocused;
private Channel mChannel;
private Program mProgram;
- private boolean mExtendViewOnFocus;
private final MainActivity mMainActivity;
public ChannelCardView(Context context) {
@@ -70,17 +62,8 @@ public class ChannelCardView extends BaseCardView<Channel> {
public ChannelCardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
-
mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width);
mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.card_image_layout_height);
- mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height);
- mExtendedCardHeight = getResources().getDimensionPixelSize(
- R.dimen.card_layout_height_extended);
- mProgramNameViewHeight = getResources().getDimensionPixelSize(
- R.dimen.card_meta_layout_height);
- mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset(
- R.dimen.card_meta_layout_height_extended);
-
mMainActivity = (MainActivity) context;
}
@@ -90,8 +73,6 @@ public class ChannelCardView extends BaseCardView<Channel> {
mImageView = (ImageView) findViewById(R.id.image);
mGradientView = findViewById(R.id.image_gradient);
mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name);
- mMetaViewFocused = (TextView) findViewById(R.id.channel_title_focused);
- mMetaViewUnfocused = (TextView) findViewById(R.id.channel_title_unfocused);
mProgressBar = (ProgressBar) findViewById(R.id.progress);
}
@@ -110,26 +91,18 @@ public class ChannelCardView extends BaseCardView<Channel> {
mGradientView.setVisibility(View.GONE);
mProgressBar.setVisibility(GONE);
- setMetaViewEnabled(true);
+ setTextViewEnabled(true);
if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled()
&& mChannel.isLocked()) {
- setMetaViewText(R.string.program_title_for_blocked_channel);
+ setText(R.string.program_title_for_blocked_channel);
return;
} else {
- setMetaViewText("");
+ setText("");
}
updateProgramInformation();
- mMetaViewFocused.measure(
- MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- if (mExtendViewOnFocus = mMetaViewFocused.getLineCount() > 1) {
- setMetaViewFocusedAlpha(selected ? 1f : 0f);
- } else {
- setMetaViewFocusedAlpha(1f);
- }
-
- // Call super.onBind() at the end in order to make getCardHeight() return a proper value.
+ // Call super.onBind() at the end intentionally. In order to correctly handle extension of
+ // text view, text should be set before calling super.onBind.
super.onBind(channel, selected);
}
@@ -153,40 +126,16 @@ public class ChannelCardView extends BaseCardView<Channel> {
mGradientView.setVisibility(View.VISIBLE);
}
- @Override
- protected void onFocusAnimationStart(boolean selected) {
- if (mExtendViewOnFocus) {
- setMetaViewFocusedAlpha(selected ? 1f : 0f);
- }
- }
-
- @Override
- protected void onSetFocusAnimatedValue(float animatedValue) {
- super.onSetFocusAnimatedValue(animatedValue);
- if (mExtendViewOnFocus) {
- ViewGroup.LayoutParams params = mMetaViewUnfocused.getLayoutParams();
- params.height = Math.round(mProgramNameViewHeight
- + (mExtendedTextViewCardHeight - mProgramNameViewHeight) * animatedValue);
- setMetaViewLayoutParams(params);
- setMetaViewFocusedAlpha(animatedValue);
- }
- }
-
- @Override
- protected float getCardHeight() {
- return (mExtendViewOnFocus && isFocused()) ? mExtendedCardHeight : mCardHeight;
- }
-
private void updateProgramInformation() {
if (mChannel == null) {
return;
}
mProgram = mMainActivity.getProgramDataManager().getCurrentProgram(mChannel.getId());
if (mProgram == null || TextUtils.isEmpty(mProgram.getTitle())) {
- setMetaViewEnabled(false);
- setMetaViewText(R.string.program_title_for_no_information);
+ setTextViewEnabled(false);
+ setText(R.string.program_title_for_no_information);
} else {
- setMetaViewText(mProgram.getTitle());
+ setText(mProgram.getTitle());
}
if (mProgram == null) {
@@ -217,29 +166,4 @@ public class ChannelCardView extends BaseCardView<Channel> {
createProgramPosterArtCallback(this, mProgram));
}
}
-
- private void setMetaViewLayoutParams(ViewGroup.LayoutParams params) {
- mMetaViewFocused.setLayoutParams(params);
- mMetaViewUnfocused.setLayoutParams(params);
- }
-
- private void setMetaViewText(String text) {
- mMetaViewFocused.setText(text);
- mMetaViewUnfocused.setText(text);
- }
-
- private void setMetaViewText(int resId) {
- mMetaViewFocused.setText(resId);
- mMetaViewUnfocused.setText(resId);
- }
-
- private void setMetaViewEnabled(boolean enabled) {
- mMetaViewFocused.setEnabled(enabled);
- mMetaViewUnfocused.setEnabled(enabled);
- }
-
- private void setMetaViewFocusedAlpha(float focusedAlpha) {
- mMetaViewFocused.setAlpha(focusedAlpha);
- mMetaViewUnfocused.setAlpha(1f - focusedAlpha);
- }
}
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index 47c413a3..c8e1bd05 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -141,6 +141,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener);
} else if (viewType == R.layout.menu_card_dvr) {
viewHolder.itemView.setOnClickListener(mDvrOnClickListener);
+ SimpleCardView view = (SimpleCardView) viewHolder.itemView;
+ view.setText(R.string.channels_item_dvr);
} else {
viewHolder.itemView.setTag(getItemList().get(position));
viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
@@ -163,10 +165,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
// Sometimes applicationInfo can be null. b/28932537
&& inputManager.getTvInputAppInfo(currentChannel.getInputId()) != null;
boolean showDvrCard = false;
- if (mDvrDataManager != null && !(mDvrDataManager.getRecordedPrograms().isEmpty()
- && mDvrDataManager.getStartedRecordings().isEmpty()
- && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty()
- && mDvrDataManager.getSeriesRecordings().isEmpty())) {
+ if (mDvrDataManager != null) {
for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) {
if (info.canRecord()) {
showDvrCard = true;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 0da10ff8..1160a5b5 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -179,7 +179,9 @@ public class Menu {
mMenuView.onShow(reason, rowIdToSelect, mAnimationDisabledForTest ? null : new Runnable() {
@Override
public void run() {
- mShowAnimator.start();
+ if (isActive()) {
+ mShowAnimator.start();
+ }
}
});
scheduleHide();
@@ -189,6 +191,9 @@ public class Menu {
* Closes the menu.
*/
public void hide(boolean withAnimation) {
+ if (mShowAnimator.isStarted()) {
+ mShowAnimator.cancel();
+ }
if (!isActive()) {
return;
}
diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java
index 7cdbfe9e..97dea29a 100644
--- a/src/com/android/tv/menu/MenuRowView.java
+++ b/src/com/android/tv/menu/MenuRowView.java
@@ -35,25 +35,6 @@ public abstract class MenuRowView extends LinearLayout {
private static final String TAG = "MenuRowView";
private static final boolean DEBUG = false;
- /**
- * For setting ListView visible, and TitleView visible with the selected text size and color
- * without animation.
- */
- public static final int ANIM_NONE_SELECTED = 1;
- /**
- * For setting ListView gone, and TitleView visible with the deselected text size and color
- * without animation.
- */
- public static final int ANIM_NONE_DESELECTED = 2;
- /**
- * An animation for the selected item list view.
- */
- public static final int ANIM_SELECTED = 3;
- /**
- * An animation for the deselected item list view.
- */
- public static final int ANIM_DESELECTED = 4;
-
private TextView mTitleView;
private View mContentsView;
diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java
index 23f0373f..075b299e 100644
--- a/src/com/android/tv/menu/MenuUpdater.java
+++ b/src/com/android/tv/menu/MenuUpdater.java
@@ -20,16 +20,7 @@ import android.content.Context;
import android.support.annotation.Nullable;
import com.android.tv.ChannelTuner;
-import com.android.tv.TvApplication;
-import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Channel;
-import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
-import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
-import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
-import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
-import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener;
@@ -43,72 +34,8 @@ public class MenuUpdater {
@Nullable
private final TunableTvView mTvView;
private final Menu mMenu;
- @Nullable
- private final DvrDataManager mDvrDataManager;
private ChannelTuner mChannelTuner;
- private final OnScreenBlockingChangedListener mOnScreenBlockingChangeListener =
- new OnScreenBlockingChangedListener() {
- @Override
- public void onScreenBlockingChanged(boolean blocked) {
- mMenu.update(PlayControlsRow.ID);
- }
- };
- private final OnRecordedProgramLoadFinishedListener mRecordedProgramLoadedListener =
- new OnRecordedProgramLoadFinishedListener() {
- @Override
- public void onRecordedProgramLoadFinished() {
- mMenu.update(ChannelsRow.ID);
- }
- };
- private final RecordedProgramListener mRecordedProgramListener =
- new RecordedProgramListener() {
- @Override
- public void onRecordedProgramAdded(RecordedProgram recordedProgram) {
- mMenu.update(ChannelsRow.ID);
- }
-
- @Override
- public void onRecordedProgramChanged(RecordedProgram recordedProgram) { }
-
- @Override
- public void onRecordedProgramRemoved(RecordedProgram recordedProgram) {
- if (mDvrDataManager != null && mDvrDataManager.getRecordedPrograms().isEmpty()
- && mDvrDataManager.getStartedRecordings().isEmpty()
- && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty()
- && mDvrDataManager.getSeriesRecordings().isEmpty()) {
- mMenu.update(ChannelsRow.ID);
- }
- }
- };
- private final OnDvrScheduleLoadFinishedListener mDvrScheduleLoadedListener =
- new OnDvrScheduleLoadFinishedListener() {
- @Override
- public void onDvrScheduleLoadFinished() {
- mMenu.update(ChannelsRow.ID);
- }
- };
- private final ScheduledRecordingListener mScheduledRecordingListener =
- new ScheduledRecordingListener() {
- @Override
- public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
- mMenu.update(ChannelsRow.ID);
- }
-
- @Override
- public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
- if (mDvrDataManager != null && mDvrDataManager.getRecordedPrograms().isEmpty()
- && mDvrDataManager.getStartedRecordings().isEmpty()
- && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty()
- && mDvrDataManager.getSeriesRecordings().isEmpty()) {
- mMenu.update(ChannelsRow.ID);
- }
- }
-
- @Override
- public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { }
- };
-
private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
@Override
public void onLoadFinished() {}
@@ -131,16 +58,12 @@ public class MenuUpdater {
mTvView = tvView;
mMenu = menu;
if (mTvView != null) {
- mTvView.setOnScreenBlockedListener(mOnScreenBlockingChangeListener);
- }
- if (CommonFeatures.DVR.isEnabled(context)) {
- mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
- mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadedListener);
- mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener);
- mDvrDataManager.addRecordedProgramLoadFinishedListener(mRecordedProgramLoadedListener);
- mDvrDataManager.addRecordedProgramListener(mRecordedProgramListener);
- } else {
- mDvrDataManager = null;
+ mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() {
+ @Override
+ public void onScreenBlockingChanged(boolean blocked) {
+ mMenu.update(PlayControlsRow.ID);
+ }
+ });
}
}
@@ -163,13 +86,6 @@ public class MenuUpdater {
* Called at the end of the menu's lifetime.
*/
public void release() {
- if (mDvrDataManager != null) {
- mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
- mDvrDataManager.removeRecordedProgramListener(mRecordedProgramListener);
- mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrScheduleLoadedListener);
- mDvrDataManager
- .removeRecordedProgramLoadFinishedListener(mRecordedProgramLoadedListener);
- }
if (mChannelTuner != null) {
mChannelTuner.removeListener(mChannelTunerListener);
}
diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java
index 4c612520..ee0b036e 100644
--- a/src/com/android/tv/menu/MenuView.java
+++ b/src/com/android/tv/menu/MenuView.java
@@ -24,6 +24,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import com.android.tv.menu.Menu.MenuShowReason;
@@ -57,6 +58,8 @@ public class MenuView extends FrameLayout implements IMenuView {
public MenuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutInflater = LayoutInflater.from(context);
+ // Set hardware layer type for smooth animation of lots of views.
+ setLayerType(LAYER_TYPE_HARDWARE, null);
getViewTreeObserver().addOnGlobalFocusChangeListener(new OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
@@ -129,7 +132,15 @@ public class MenuView extends FrameLayout implements IMenuView {
// Make the selected row have the focus.
requestFocus();
if (runnableAfterShow != null) {
- runnableAfterShow.run();
+ getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ // Start show animation after layout finishes for smooth animation because the
+ // layout can take long time.
+ runnableAfterShow.run();
+ }
+ });
}
mLayoutManager.onMenuShow();
}
@@ -183,6 +194,16 @@ public class MenuView extends FrameLayout implements IMenuView {
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
+ @Override
+ public void focusableViewAvailable(View v) {
+ // Workaround of b/30788222 and b/32074688.
+ // The re-layout of RecyclerView gives the focus to the card view even when the menu is not
+ // visible. Don't report focusable view when the menu is not visible.
+ if (getVisibility() == VISIBLE) {
+ super.focusableViewAvailable(v);
+ }
+ }
+
private void setSelectedPosition(int position) {
mLayoutManager.setSelectedPosition(position);
}
diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java
index 95c07b74..aff39db3 100644
--- a/src/com/android/tv/menu/PlayControlsButton.java
+++ b/src/com/android/tv/menu/PlayControlsButton.java
@@ -129,6 +129,7 @@ public class PlayControlsButton extends FrameLayout {
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
mButton.setEnabled(enabled);
+ mButton.setFocusable(enabled);
mIcon.setEnabled(enabled);
mIcon.setAlpha(enabled ? ALPHA_ENABLED : ALPHA_DISABLED);
mLabel.setEnabled(enabled);
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index a19cbf89..a620d4dd 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -40,6 +40,8 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.HalfSizedDialogFragment;
import com.android.tv.menu.Menu.MenuShowReason;
import com.android.tv.ui.TunableTvView;
import com.android.tv.util.Utils;
@@ -130,10 +132,11 @@ public class PlayControlsRowView extends MenuRowView {
res.getDimensionPixelSize(R.dimen.play_controls_button_compact_margin);
if (CommonFeatures.DVR.isEnabled(context)) {
mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrManager = TvApplication.getSingletons(context).getDvrManager();
} else {
mDvrDataManager = null;
+ mDvrManager = null;
}
- mDvrManager = TvApplication.getSingletons(context).getDvrManager();
mMainActivity = (MainActivity) context;
}
@@ -249,7 +252,7 @@ public class PlayControlsRowView extends MenuRowView {
private boolean isCurrentChannelRecording() {
Channel currentChannel = mMainActivity.getCurrentChannel();
- return currentChannel != null
+ return currentChannel != null && mDvrManager != null
&& mDvrManager.getCurrentRecording(currentChannel.getId()) != null;
}
@@ -259,10 +262,11 @@ public class PlayControlsRowView extends MenuRowView {
TvApplication.getSingletons(getContext()).getTracker().sendMenuClicked(isRecording ?
R.string.channels_item_record_start : R.string.channels_item_record_stop);
if (!isRecording) {
- if (!mDvrManager.isChannelRecordable(currentChannel)) {
+ if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) {
Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel,
Toast.LENGTH_SHORT).show();
- } else {
+ } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity,
+ currentChannel.getInputId())) {
Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager()
.getCurrentProgram(currentChannel.getId());
if (program == null) {
@@ -274,16 +278,25 @@ public class PlayControlsRowView extends MenuRowView {
Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show();
}
}
- } else {
- DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel);
+ } else if (currentChannel != null) {
+ DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(),
+ DvrStopRecordingFragment.REASON_USER_STOP,
+ new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
+ ScheduledRecording currentRecording =
+ mDvrManager.getCurrentRecording(
+ currentChannel.getId());
+ if (currentRecording != null) {
+ mDvrManager.stopRecording(currentRecording);
+ }
+ }
+ }
+ });
}
}
- private boolean needToShowRecordButton() {
- return CommonFeatures.DVR.isEnabled(getContext())
- && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel());
- }
-
private void initializeButton(PlayControlsButton button, int imageResId,
int descriptionId, Integer focusedIconColor, Runnable clickAction) {
button.setImageResId(imageResId);
@@ -609,7 +622,8 @@ public class PlayControlsRowView extends MenuRowView {
}
private void updateRecordButton() {
- if (!needToShowRecordButton()) {
+ if (!(mDvrManager != null
+ && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) {
mRecordButton.setVisibility(View.GONE);
updateButtonMargin();
return;
diff --git a/src/com/android/tv/menu/SetupCardView.java b/src/com/android/tv/menu/SetupCardView.java
deleted file mode 100644
index 7ad5e9d0..00000000
--- a/src/com/android/tv/menu/SetupCardView.java
+++ /dev/null
@@ -1,53 +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.menu;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-import com.android.tv.R;
-import com.android.tv.data.Channel;
-
-/**
- * A view to render a guide card.
- */
-public class SetupCardView extends BaseCardView<Channel> {
- private static final String TAG = "GuideCardView";
- private static final boolean DEBUG = false;
-
- private static final int INVALID_COUNT = -1;
-
- private final float mCardHeight;
-
- public SetupCardView(Context context) {
- this(context, null, 0);
- }
-
- public SetupCardView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public SetupCardView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- mCardHeight = getResources().getDimension(R.dimen.card_layout_height);
- }
-
- @Override
- protected float getCardHeight() {
- return mCardHeight;
- }
-}
diff --git a/src/com/android/tv/menu/SimpleCardView.java b/src/com/android/tv/menu/SimpleCardView.java
index 24a44244..c99834be 100644
--- a/src/com/android/tv/menu/SimpleCardView.java
+++ b/src/com/android/tv/menu/SimpleCardView.java
@@ -19,16 +19,12 @@ package com.android.tv.menu;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.tv.R;
import com.android.tv.data.Channel;
/**
* A view to render a guide card.
*/
public class SimpleCardView extends BaseCardView<Channel> {
- private static final String TAG = "GuideCardView";
- private static final boolean DEBUG = false;
- private final float mCardHeight;
public SimpleCardView(Context context) {
this(context, null, 0);
@@ -40,11 +36,5 @@ public class SimpleCardView extends BaseCardView<Channel> {
public SimpleCardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mCardHeight = getResources().getDimension(R.dimen.card_layout_height);
- }
-
- @Override
- protected float getCardHeight() {
- return mCardHeight;
}
}
diff --git a/src/com/android/tv/onboarding/NewSourcesFragment.java b/src/com/android/tv/onboarding/NewSourcesFragment.java
index 1b14c114..8509b50c 100644
--- a/src/com/android/tv/onboarding/NewSourcesFragment.java
+++ b/src/com/android/tv/onboarding/NewSourcesFragment.java
@@ -26,7 +26,6 @@ import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.ui.setup.SetupActionHelper;
import com.android.tv.util.SetupUtils;
@@ -48,8 +47,6 @@ public class NewSourcesFragment extends Fragment {
*/
public static final int ACTION_SKIP = 2;
- private OnActionClickListener mOnActionClickListener;
-
public NewSourcesFragment() {
setAllowEnterTransitionOverlap(false);
setAllowReturnTransitionOverlap(false);
diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java
index 41467376..45205c4c 100644
--- a/src/com/android/tv/onboarding/OnboardingActivity.java
+++ b/src/com/android/tv/onboarding/OnboardingActivity.java
@@ -44,7 +44,6 @@ public class OnboardingActivity extends SetupActivity {
private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion";
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
- private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
private static final int SHOW_RIPPLE_DURATION_MS = 266;
@@ -82,10 +81,19 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (!PermissionUtils.hasAccessAllEpg(this)
- && checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
- != PackageManager.PERMISSION_GRANTED) {
- requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS},
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
+ mInputManager = singletons.getTvInputManagerHelper();
+ if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
+ mChannelDataManager = singletons.getChannelDataManager();
+ // Make the channels of the new inputs which have been setup outside Live TV
+ // browsable.
+ if (mChannelDataManager.isDbLoadFinished()) {
+ SetupUtils.getInstance(this).markNewChannelsBrowsable();
+ } else {
+ mChannelDataManager.addListener(mChannelListener);
+ }
+ } else {
+ requestPermissions(new String[] {PermissionUtils.PERMISSION_READ_TV_LISTINGS},
PERMISSIONS_REQUEST_READ_TV_LISTINGS);
}
}
@@ -101,16 +109,6 @@ public class OnboardingActivity extends SetupActivity {
@Override
protected Fragment onCreateInitialFragment() {
if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) {
- ApplicationSingletons singletons = TvApplication.getSingletons(this);
- mChannelDataManager = singletons.getChannelDataManager();
- mInputManager = singletons.getTvInputManagerHelper();
- // Make the channels of the new inputs which have been setup outside Live TV
- // browsable.
- if (mChannelDataManager.isDbLoadFinished()) {
- SetupUtils.getInstance(this).markNewChannelsBrowsable();
- } else {
- mChannelDataManager.addListener(mChannelListener);
- }
return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment()
: new SetupSourcesFragment();
}
diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java
new file mode 100644
index 00000000..7e627410
--- /dev/null
+++ b/src/com/android/tv/setup/SystemSetupActivity.java
@@ -0,0 +1,124 @@
+/*
+ * 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.setup;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
+import android.os.Bundle;
+import android.widget.Toast;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
+import com.android.tv.SetupPassthroughActivity;
+import com.android.tv.common.TvCommonUtils;
+import com.android.tv.common.ui.setup.SetupActivity;
+import com.android.tv.common.ui.setup.SetupFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.TvApplication;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.onboarding.SetupSourcesFragment;
+import com.android.tv.util.OnboardingUtils;
+import com.android.tv.util.SetupUtils;
+import com.android.tv.util.TvInputManagerHelper;
+
+/**
+ * A activity to start input sources setup fragment for initial setup flow.
+ */
+public class SystemSetupActivity extends SetupActivity {
+ private static final String SYSTEM_SETUP =
+ "com.android.tv.action.LAUNCH_SYSTEM_SETUP";
+ private static final int SHOW_RIPPLE_DURATION_MS = 266;
+ private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
+
+ private TvInputManagerHelper mInputManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ if (!SYSTEM_SETUP.equals(intent.getAction())) {
+ finish();
+ return;
+ }
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
+ mInputManager = singletons.getTvInputManagerHelper();
+ }
+
+ @Override
+ protected Fragment onCreateInitialFragment() {
+ return new SetupSourcesFragment();
+ }
+
+ private void showMerchantCollection() {
+ executeActionWithDelay(new Runnable() {
+ @Override
+ public void run() {
+ startActivity(OnboardingUtils.ONLINE_STORE_INTENT);
+ }
+ }, SHOW_RIPPLE_DURATION_MS);
+ }
+
+ @Override
+ public boolean executeAction(String category, int actionId, Bundle params) {
+ switch (category) {
+ case SetupSourcesFragment.ACTION_CATEGORY:
+ switch (actionId) {
+ case SetupSourcesFragment.ACTION_ONLINE_STORE:
+ showMerchantCollection();
+ return true;
+ case SetupSourcesFragment.ACTION_SETUP_INPUT: {
+ String inputId = params.getString(
+ SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID);
+ TvInputInfo input = mInputManager.getTvInputInfo(inputId);
+ Intent intent = TvCommonUtils.createSetupIntent(input);
+ if (intent == null) {
+ Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ // Even though other app can handle the intent, the setup launched by Live
+ // channels should go through Live channels SetupPassthroughActivity.
+ intent.setComponent(new ComponentName(this,
+ SetupPassthroughActivity.class));
+ try {
+ // Now we know that the user intends to set up this input. Grant
+ // permission for writing EPG data.
+ SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName);
+ startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this,
+ getString(R.string.msg_unable_to_start_setup_activity,
+ input.loadLabel(this)), Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ case SetupMultiPaneFragment.ACTION_DONE: {
+ // To make sure user can finish setup flow, set result as RESULT_OK.
+ setResult(Activity.RESULT_OK);
+ finish();
+ return true;
+ }
+ }
+ break;
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/tv/tuner/TunerPreferenceProvider.java b/src/com/android/tv/tuner/TunerPreferenceProvider.java
index 3d289bb2..3a3561b6 100644
--- a/src/com/android/tv/tuner/TunerPreferenceProvider.java
+++ b/src/com/android/tv/tuner/TunerPreferenceProvider.java
@@ -38,8 +38,6 @@ public class TunerPreferenceProvider extends ContentProvider {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "usbtuner_preferences.db";
private static final String PREFERENCES_TABLE = "preferences";
- private static final String PREFERENCES_TABLE_ID_INDEX = "preferences_id_index";
- private static final String PREFERENCES_TABLE_KEY_INDEX = "preferences_key_index";
private static final int MATCH_PREFERENCE = 1;
private static final int MATCH_PREFERENCE_KEY = 2;
diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java
index 7a5518c8..1547e3ae 100644
--- a/src/com/android/tv/tuner/TunerPreferences.java
+++ b/src/com/android/tv/tuner/TunerPreferences.java
@@ -41,6 +41,7 @@ public class TunerPreferences {
private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count";
private static final String PREFS_KEY_SCAN_DONE = "scan_done";
private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup";
+ private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream";
private static final String SHARED_PREFS_NAME = "com.android.tv.tuner.preferences";
@@ -202,6 +203,28 @@ public class TunerPreferences {
}
}
+ @MainThread
+ public static boolean getStoreTsStream(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
+ } else {
+ return getSharedPreferences(context)
+ .getBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, false);
+ }
+ }
+
+ @MainThread
+ public static void setStoreTsStream(Context context, boolean shouldStore) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
+ } else {
+ getSharedPreferences(context).edit()
+ .putBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, shouldStore)
+ .apply();
+ }
+ }
+
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
}
@@ -266,6 +289,7 @@ public class TunerPreferences {
break;
case PREFS_KEY_SCAN_DONE:
case PREFS_KEY_LAUNCH_SETUP:
+ case PREFS_KEY_STORE_TS_STREAM:
bundle.putBoolean(key, Boolean.parseBoolean(value));
break;
}
diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java
index 91c1f5b0..22cf2aa6 100644
--- a/src/com/android/tv/tuner/data/TunerChannel.java
+++ b/src/com/android/tv/tuner/data/TunerChannel.java
@@ -54,8 +54,6 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
"Extended Parameterized Service" };
private static final String ATSC_SERVICE_TYPE_NAME_RESERVED =
ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED];
- private static final String ATSC_SERVICE_TYPE_NAME_DIGITAL_TELEVISION =
- ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION];
public static final int INVALID_FREQUENCY = -1;
diff --git a/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java b/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java
deleted file mode 100644
index e7c11e38..00000000
--- a/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java
+++ /dev/null
@@ -1,57 +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.tuner.exoplayer;
-
-
-import android.media.MediaDataSource;
-
-import com.google.android.exoplayer.C;
-import com.google.android.exoplayer.upstream.DataSource;
-import com.google.android.exoplayer.upstream.DataSpec;
-
-import java.io.IOException;
-
-/**
- * A DataSource adapter implementation by using {@link MediaDataSource}.
- */
-public class DataSourceAdapter implements DataSource {
- private MediaDataSource mMediaDataSource;
- private long mReadPosition;
-
- public DataSourceAdapter(MediaDataSource mediaDataSource) {
- mMediaDataSource = mediaDataSource;
- }
-
- @Override
- public long open(DataSpec dataSpec) throws IOException {
- return C.LENGTH_UNBOUNDED;
- }
-
- @Override
- public void close() throws IOException {
- mMediaDataSource.close();
- }
-
- @Override
- public int read(byte[] buffer, int offset, int readLength) throws IOException {
- int ret = mMediaDataSource.readAt(mReadPosition, buffer, offset, readLength);
- if (ret > 0) {
- mReadPosition += ret;
- return ret;
- }
- return C.RESULT_END_OF_INPUT;
- }
-}
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index 4eb0d32c..c105e222 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -16,10 +16,12 @@
package com.android.tv.tuner.exoplayer;
-import android.media.MediaDataSource;
-import android.media.tv.TvContract;
+import android.net.Uri;
import android.os.ConditionVariable;
import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
import android.os.SystemClock;
import com.google.android.exoplayer.C;
@@ -28,6 +30,7 @@ import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.extractor.ExtractorSampleSource;
+import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener;
import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultAllocator;
@@ -41,6 +44,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -48,36 +52,49 @@ import java.util.concurrent.atomic.AtomicLong;
* For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}.
*/
public class ExoPlayerSampleExtractor implements SampleExtractor {
- private static final String TAG = "ExoPlayerSampleExtractor";
+ private static final String TAG = "ExoPlayerSampleExtracto";
// Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer.
private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024;
// Buffer segment count for sample source. Copied from demo implementation of ExoPlayer.
private static final int BUFFER_SEGMENT_COUNT = 256;
- private static final AtomicLong ID_COUNTER = new AtomicLong(0);
-
- private final ExtractorThread mExtractorThread;
- private final BufferManager.SampleBuffer mSampleBuffer;
+ private final HandlerThread mSourceReaderThread;
private final long mId;
- private final List<MediaFormat> mTrackFormats = new ArrayList<>();
- private final SampleSource.SampleSourceReader mSampleSourceReader;
+ private final Handler.Callback mSourceReaderWorker;
- private boolean mReleased;
- private boolean mOnCompletionCalled;
+ private BufferManager.SampleBuffer mSampleBuffer;
+ private Handler mSourceReaderHandler;
+ private volatile boolean mPrepared;
+ private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
+ private IOException mExceptionOnPrepare;
+ private List<MediaFormat> mTrackFormats;
private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
private OnCompletionListener mOnCompletionListener;
private Handler mOnCompletionListenerHandler;
+ private IOException mError;
- public ExoPlayerSampleExtractor(DataSource source, BufferManager bufferManager,
+ public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
PlaybackBufferListener bufferListener, boolean isRecording) {
- mId = ID_COUNTER.incrementAndGet();
+ // It'll be used as a timeshift file chunk name's prefix.
+ mId = System.currentTimeMillis();
Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
- mSampleSourceReader = new ExtractorSampleSource(TvContract.Programs.CONTENT_URI, source,
- allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, null, null, 0);
- mExtractorThread = new ExtractorThread();
+ EventListener eventListener = new EventListener() {
+
+ @Override
+ public void onLoadError(int sourceId, IOException e) {
+ mError = e;
+ }
+ };
+
+ mSourceReaderThread = new HandlerThread("SourceReaderThread");
+ mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
+ allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
+ // Do not create a handler if we not on a looper. e.g. test.
+ Looper.myLooper() != null ? new Handler() : null,
+ eventListener, 0));
if (isRecording) {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
RecordingSampleBuffer.BUFFER_REASON_RECORDING);
@@ -97,35 +114,114 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mOnCompletionListenerHandler = handler;
}
- private class ExtractorThread extends Thread {
- private static final int FETCH_SAMPLE_INTERVAL_MS = 50;
- private volatile boolean mQuitRequested = false;
+ private class SourceReaderWorker implements Handler.Callback {
+ public static final int MSG_PREPARE = 1;
+ public static final int MSG_FETCH_SAMPLES = 2;
+ public static final int MSG_RELEASE = 3;
+ private static final int RETRY_INTERVAL_MS = 50;
+
+ private final SampleSource mSampleSource;
+ private SampleSource.SampleSourceReader mSampleSourceReader;
+ private boolean[] mTrackMetEos;
+ private boolean mMetEos = false;
private long mCurrentPosition;
- public ExtractorThread() {
- super("ExtractorThread");
+ public SourceReaderWorker(SampleSource sampleSource) {
+ mSampleSource = sampleSource;
}
@Override
- public void run() {
- SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
- ConditionVariable conditionVariable = new ConditionVariable();
- int trackCount = mSampleSourceReader.getTrackCount();
- while (!mQuitRequested) {
- boolean didSomething = false;
- for (int i = 0; i < trackCount; ++i) {
- if(SampleSource.NOTHING_READ != fetchSample(i, sample, conditionVariable)) {
- didSomething = true;
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PREPARE:
+ mPrepared = prepare();
+ if (!mPrepared && mExceptionOnPrepare == null) {
+ mSourceReaderHandler
+ .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS);
+ } else{
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
}
- }
- if (!didSomething) {
- try {
- Thread.sleep(FETCH_SAMPLE_INTERVAL_MS);
- } catch (InterruptedException e) {
+ return true;
+ case MSG_FETCH_SAMPLES:
+ boolean didSomething = false;
+ SampleHolder sample = new SampleHolder(
+ SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ ConditionVariable conditionVariable = new ConditionVariable();
+ int trackCount = mSampleSourceReader.getTrackCount();
+ for (int i = 0; i < trackCount; ++i) {
+ if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
+ != fetchSample(i, sample, conditionVariable)) {
+ if (mMetEos) {
+ // If mMetEos was on during fetchSample() due to an error,
+ // fetching from other tracks is not necessary.
+ break;
+ }
+ didSomething = true;
+ }
+ }
+ if (!mMetEos) {
+ if (didSomething) {
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ } else {
+ mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES,
+ RETRY_INTERVAL_MS);
+ }
+ } else {
+ notifyCompletionIfNeeded(false);
+ }
+ return true;
+ case MSG_RELEASE:
+ if (mSampleSourceReader != null) {
+ if (mPrepared) {
+ // ExtractorSampleSource expects all the tracks should be disabled
+ // before releasing.
+ int count = mSampleSourceReader.getTrackCount();
+ for (int i = 0; i < count; ++i) {
+ mSampleSourceReader.disable(i);
+ }
+ }
+ mSampleSourceReader.release();
+ mSampleSourceReader = null;
}
+ cleanUp();
+ mSourceReaderHandler.removeCallbacksAndMessages(null);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean prepare() {
+ if (mSampleSourceReader == null) {
+ mSampleSourceReader = mSampleSource.register();
+ }
+ if(!mSampleSourceReader.prepare(0)) {
+ return false;
+ }
+ if (mTrackFormats == null) {
+ int trackCount = mSampleSourceReader.getTrackCount();
+ mTrackMetEos = new boolean[trackCount];
+ List<MediaFormat> trackFormats = new ArrayList<>();
+ for (int i = 0; i < trackCount; i++) {
+ trackFormats.add(mSampleSourceReader.getFormat(i));
+ mSampleSourceReader.enable(i, 0);
+
+ }
+ mTrackFormats = trackFormats;
+ List<String> ids = new ArrayList<>();
+ for (int i = 0; i < mTrackFormats.size(); i++) {
+ ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
+ }
+ try {
+ mSampleBuffer.init(ids, mTrackFormats);
+ } catch (IOException e) {
+ // In this case, we will not schedule any further operation.
+ // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
+ // call release() eventually.
+ mExceptionOnPrepare = e;
+ return false;
}
}
- cleanUp();
+ return true;
}
private int fetchSample(int track, SampleHolder sample,
@@ -150,20 +246,24 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
queueSample(track, sample, conditionVariable);
} catch (IOException e) {
mLastExtractedPositionUsMap.clear();
- mQuitRequested = true;
+ mMetEos = true;
mSampleBuffer.setEos();
}
} else if (ret == SampleSource.END_OF_STREAM) {
- mQuitRequested = true;
- mSampleBuffer.setEos();
+ mTrackMetEos[track] = true;
+ for (int i = 0; i < mTrackMetEos.length; ++i) {
+ if (!mTrackMetEos[i]) {
+ break;
+ }
+ if (i == mTrackMetEos.length -1) {
+ mMetEos = true;
+ mSampleBuffer.setEos();
+ }
+ }
}
// TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
return ret;
}
-
- public void quit() {
- mQuitRequested = true;
- }
}
private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
@@ -179,29 +279,30 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
+ public void maybeThrowError() throws IOException {
+ if (mError != null) {
+ IOException e = mError;
+ mError = null;
+ throw e;
+ }
+ }
+
+ @Override
public boolean prepare() throws IOException {
- synchronized (this) {
- if(!mSampleSourceReader.prepare(0)) {
- return false;
- }
- int trackCount = mSampleSourceReader.getTrackCount();
- mTrackFormats.clear();
- for (int i = 0; i < trackCount; i++) {
- mTrackFormats.add(mSampleSourceReader.getFormat(i));
- mSampleSourceReader.enable(i, 0);
- }
- List<String> ids = new ArrayList<>();
- for (int i = 0; i < trackCount; i++) {
- ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
- }
- mSampleBuffer.init(ids, mTrackFormats);
+ if (!mSourceReaderThread.isAlive()) {
+ mSourceReaderThread.start();
+ mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(),
+ mSourceReaderWorker);
+ mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE);
+ }
+ if (mExceptionOnPrepare != null) {
+ throw mExceptionOnPrepare;
}
- mExtractorThread.start();
- return true;
+ return mPrepared;
}
@Override
- public synchronized List<MediaFormat> getTrackFormats() {
+ public List<MediaFormat> getTrackFormats() {
return mTrackFormats;
}
@@ -243,27 +344,45 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
@Override
public void release() {
- synchronized (this) {
- mReleased = true;
- }
- if (mExtractorThread.isAlive()) {
- mExtractorThread.quit();
+ if (mSourceReaderThread.isAlive()) {
+ mSourceReaderHandler.removeCallbacksAndMessages(null);
+ mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE);
+ mSourceReaderThread.quitSafely();
+ // Return early in this case so that session worker can start working on the next
+ // request as early as it can. The clean up will be done in the reader thread while
+ // handling MSG_RELEASE.
} else {
cleanUp();
}
}
- private void onCompletion(final boolean result, long lastExtractedPositionUs) {
- final OnCompletionListener listener = mOnCompletionListener;
- if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
- mOnCompletionListenerHandler.post(new Runnable() {
- @Override
- public void run() {
- listener.onCompletion(result, lastExtractedPositionUs);
- }
- });
+ private void cleanUp() {
+ boolean result = true;
+ try {
+ if (mSampleBuffer != null) {
+ mSampleBuffer.release();
+ mSampleBuffer = null;
+ }
+ } catch (IOException e) {
+ result = false;
+ }
+ notifyCompletionIfNeeded(result);
+ setOnCompletionListener(null, null);
+ }
+
+ private void notifyCompletionIfNeeded(final boolean result) {
+ if (!mOnCompletionCalled.getAndSet(true)) {
+ final OnCompletionListener listener = mOnCompletionListener;
+ final long lastExtractedPositionUs = getLastExtractedPositionUs();
+ if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
+ mOnCompletionListenerHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onCompletion(result, lastExtractedPositionUs);
+ }
+ });
+ }
}
- mOnCompletionCalled = true;
}
private long getLastExtractedPositionUs() {
@@ -276,23 +395,4 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
return lastExtractedPositionUs;
}
-
- private synchronized void cleanUp() {
- if (!mReleased) {
- if (!mOnCompletionCalled) {
- onCompletion(false, getLastExtractedPositionUs());
- }
- return;
- }
- boolean result = true;
- try {
- mSampleBuffer.release();
- } catch (IOException e) {
- result = false;
- }
- if (!mOnCompletionCalled) {
- onCompletion(result, getLastExtractedPositionUs());
- }
- setOnCompletionListener(null, null);
- }
}
diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index a5059bc2..ec7b4b16 100644
--- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -55,6 +55,11 @@ public class FileSampleExtractor implements SampleExtractor{
}
@Override
+ public void maybeThrowError() throws IOException {
+ // Do nothing.
+ }
+
+ @Override
public boolean prepare() throws IOException {
ArrayList<Pair<String, android.media.MediaFormat>> trackInfos =
mBufferManager.readTrackInfoFiles();
diff --git a/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java
deleted file mode 100644
index 487844b4..00000000
--- a/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java
+++ /dev/null
@@ -1,283 +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.tuner.exoplayer;
-
-import android.media.MediaDataSource;
-import android.media.MediaExtractor;
-import android.os.Handler;
-import android.os.ConditionVariable;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.google.android.exoplayer.C;
-import com.google.android.exoplayer.MediaFormat;
-import com.google.android.exoplayer.MediaFormatHolder;
-import com.google.android.exoplayer.MediaFormatUtil;
-import com.google.android.exoplayer.SampleHolder;
-import com.android.tv.tuner.exoplayer.buffer.BufferManager;
-import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
-import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
-import com.android.tv.tuner.tvinput.PlaybackBufferListener;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * A class that plays and records a live stream from tuner using a {@link MediaExtractor}
- * and the private ExtractorThread class.
- */
-public class FrameworkSampleExtractor implements SampleExtractor {
- private static final String TAG = "FrameworkSampleExtractor";
-
- // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice.
- private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2;
- private static final AtomicLong ID_COUNTER = new AtomicLong(0);
-
- private final MediaDataSource mDataSource;
- private final MediaExtractor mMediaExtractor;
- private final ExtractorThread mExtractorThread;
- private final BufferManager.SampleBuffer mSampleBuffer;
- private final long mId;
- private final List<MediaFormat> mTrackFormats = new ArrayList<>();
-
- private boolean mReleased;
- private boolean mOnCompletionCalled;
- private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
- private OnCompletionListener mOnCompletionListener;
- private Handler mOnCompletionListenerHandler;
-
- public FrameworkSampleExtractor(MediaDataSource source, BufferManager bufferManager,
- PlaybackBufferListener bufferListener, boolean isRecording) {
- mId = ID_COUNTER.incrementAndGet();
- mDataSource = source;
- mMediaExtractor = new MediaExtractor();
- mExtractorThread = new ExtractorThread();
- if (isRecording) {
- mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
- RecordingSampleBuffer.BUFFER_REASON_RECORDING);
- } else {
- if (bufferManager == null || bufferManager.isDisabled()) {
- mSampleBuffer = new SimpleSampleBuffer(bufferListener);
- } else {
- mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true,
- RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK);
- }
- }
- }
-
- @Override
- public void setOnCompletionListener(OnCompletionListener listener, Handler handler) {
- mOnCompletionListener = listener;
- mOnCompletionListenerHandler = handler;
- }
-
- private class ExtractorThread extends Thread {
- private volatile boolean mQuitRequested = false;
-
- public ExtractorThread() {
- super("ExtractorThread");
- }
-
- @Override
- public void run() {
- SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
- sample.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE);
- ConditionVariable conditionVariable = new ConditionVariable();
- while (!mQuitRequested) {
- fetchSample(sample, conditionVariable);
- }
- cleanUp();
- }
-
- private void fetchSample(SampleHolder sample, ConditionVariable conditionVariable) {
- int index = mMediaExtractor.getSampleTrackIndex();
- if (index < 0) {
- Log.i(TAG, "EoS");
- mQuitRequested = true;
- mSampleBuffer.setEos();
- return;
- }
- sample.data.clear();
- sample.size = mMediaExtractor.readSampleData(sample.data, 0);
- if (sample.size < 0 || sample.size > SAMPLE_BUFFER_SIZE) {
- // Should not happen
- Log.e(TAG, "Invalid sample size: " + sample.size);
- mMediaExtractor.advance();
- return;
- }
- sample.data.position(sample.size);
- sample.timeUs = mMediaExtractor.getSampleTime();
- sample.flags = mMediaExtractor.getSampleFlags();
-
- mMediaExtractor.advance();
- try {
- Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(index);
- if (lastExtractedPositionUs == null) {
- mLastExtractedPositionUsMap.put(index, sample.timeUs);
- } else {
- mLastExtractedPositionUsMap.put(index,
- Math.max(lastExtractedPositionUs, sample.timeUs));
- }
- queueSample(index, sample, conditionVariable);
- } catch (IOException e) {
- mLastExtractedPositionUsMap.clear();
- mQuitRequested = true;
- mSampleBuffer.setEos();
- }
- }
-
- public void quit() {
- mQuitRequested = true;
- }
- }
-
- private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
- throws IOException {
- long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
- mSampleBuffer.writeSample(index, sample, conditionVariable);
-
- // Checks whether the storage has enough bandwidth for recording samples.
- if (mSampleBuffer.isWriteSpeedSlow(sample.size,
- SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
- mSampleBuffer.handleWriteSpeedSlow();
- }
- }
-
- @Override
- public boolean prepare() throws IOException {
- synchronized (this) {
- mMediaExtractor.setDataSource(mDataSource);
- int trackCount = mMediaExtractor.getTrackCount();
- mTrackFormats.clear();
- for (int i = 0; i < trackCount; i++) {
- mTrackFormats.add(MediaFormatUtil.createMediaFormat(
- mMediaExtractor.getTrackFormat(i)));
- mMediaExtractor.selectTrack(i);
- }
- List<String> ids = new ArrayList<>();
- for (int i = 0; i < trackCount; i++) {
- ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
- }
- mSampleBuffer.init(ids, mTrackFormats);
- }
- mExtractorThread.start();
- return true;
- }
-
- @Override
- public synchronized List<MediaFormat> getTrackFormats() {
- return mTrackFormats;
- }
-
- @Override
- public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) {
- outMediaFormatHolder.format = mTrackFormats.get(track);
- outMediaFormatHolder.drmInitData = null;
- }
-
- @Override
- public void selectTrack(int index) {
- mSampleBuffer.selectTrack(index);
- }
-
- @Override
- public void deselectTrack(int index) {
- mSampleBuffer.deselectTrack(index);
- }
-
- @Override
- public long getBufferedPositionUs() {
- return mSampleBuffer.getBufferedPositionUs();
- }
-
- @Override
- public boolean continueBuffering(long positionUs) {
- return mSampleBuffer.continueBuffering(positionUs);
- }
-
- @Override
- public void seekTo(long positionUs) {
- mSampleBuffer.seekTo(positionUs);
- }
-
- @Override
- public int readSample(int track, SampleHolder sampleHolder) {
- return mSampleBuffer.readSample(track, sampleHolder);
- }
-
- @Override
- public void release() {
- synchronized (this) {
- mReleased = true;
- }
- if (mExtractorThread.isAlive()) {
- mExtractorThread.quit();
-
- // We don't join here to prevent hang --- MediaExtractor is released at the thread.
- } else {
- cleanUp();
- }
- }
-
- private void onCompletion(final boolean result, long lastExtractedPositionUs) {
- final OnCompletionListener listener = mOnCompletionListener;
- if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) {
- mOnCompletionListenerHandler.post(new Runnable() {
- @Override
- public void run() {
- listener.onCompletion(result, lastExtractedPositionUs);
- }
- });
- }
- mOnCompletionCalled = true;
- }
-
- private long getLastExtractedPositionUs() {
- long lastExtractedPositionUs = Long.MAX_VALUE;
- for (long value : mLastExtractedPositionUsMap.values()) {
- lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
- }
- if (lastExtractedPositionUs == Long.MAX_VALUE) {
- lastExtractedPositionUs = C.UNKNOWN_TIME_US;
- }
- return lastExtractedPositionUs;
- }
-
- private synchronized void cleanUp() {
- if (!mReleased) {
- if (!mOnCompletionCalled) {
- onCompletion(false, getLastExtractedPositionUs());
- }
- return;
- }
- boolean result = true;
- try {
- mSampleBuffer.release();
- } catch (IOException e) {
- result = false;
- }
- if (!mOnCompletionCalled) {
- onCompletion(result, getLastExtractedPositionUs());
- }
- setOnCompletionListener(null, null);
- mMediaExtractor.release();
- }
-}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index c1a20a55..381b22e9 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -19,7 +19,6 @@ package com.android.tv.tuner.exoplayer;
import android.content.Context;
import android.media.AudioFormat;
import android.media.MediaCodec.CryptoException;
-import android.media.MediaDataSource;
import android.media.PlaybackParams;
import android.os.Handler;
import android.support.annotation.IntDef;
@@ -35,14 +34,15 @@ import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioTrack;
+import com.google.android.exoplayer.upstream.DataSource;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer;
import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer;
-import com.android.tv.tuner.source.TsMediaDataSource;
-import com.android.tv.tuner.source.TsMediaDataSourceManager;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.tvinput.EventDetector;
import java.lang.annotation.Retention;
@@ -59,7 +59,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* Interface definition for building specific track renderers.
*/
public interface RendererBuilder {
- void buildRenderers(MpegTsPlayer mpegTsPlayer, MediaDataSource dataSource,
+ void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource,
RendererBuilderCallback callback);
}
@@ -125,14 +125,13 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
private final ExoPlayer mPlayer;
private final Handler mMainHandler;
private final AudioCapabilities mAudioCapabilities;
- private final TsMediaDataSourceManager mSourceManager;
+ private final TsDataSourceManager mSourceManager;
private Listener mListener;
@RendererBuildingState private int mRendererBuildingState;
- private boolean mLastReportedPlayWhenReady;
private Surface mSurface;
- private TsMediaDataSource mMediaDataSource;
+ private TsDataSource mDataSource;
private InternalRendererBuilderCallback mBuilderCallback;
private TrackRenderer mVideoRenderer;
private TrackRenderer mAudioRenderer;
@@ -147,12 +146,12 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
*
* @param rendererBuilder the builder of track renderers
* @param handler the handler for the playback events in track renderers
- * @param sourceManager the manager for {@link MediaDataSource}
+ * @param sourceManager the manager for {@link DataSource}
* @param capabilities the {@link AudioCapabilities} of the current device
* @param listener the listener for playback state changes
*/
public MpegTsPlayer(RendererBuilder rendererBuilder, Handler handler,
- TsMediaDataSourceManager sourceManager, AudioCapabilities capabilities,
+ TsDataSourceManager sourceManager, AudioCapabilities capabilities,
Listener listener) {
mRendererBuilder = rendererBuilder;
mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS);
@@ -213,7 +212,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
- * Creates renderers and {@link MediaDataSource} and initializes player.
+ * Creates renderers and {@link DataSource} and initializes player.
* @param context a {@link Context} instance
* @param channel to play
* @param eventListener for program information which will be scanned from MPEG2-TS stream
@@ -221,14 +220,14 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
*/
public boolean prepare(Context context, TunerChannel channel,
EventDetector.EventListener eventListener) {
- TsMediaDataSource source = null;
+ TsDataSource source = null;
if (channel != null) {
source = mSourceManager.createDataSource(context, channel, eventListener);
if (source == null) {
return false;
}
}
- mMediaDataSource = source;
+ mDataSource = source;
if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) {
mPlayer.stop();
}
@@ -242,10 +241,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
- * Returns {@link TsMediaDataSource} which provides MPEG2-TS stream.
+ * Returns {@link TsDataSource} which provides MPEG2-TS stream.
*/
- public TsMediaDataSource getDataSource() {
- return mMediaDataSource;
+ public TsDataSource getDataSource() {
+ return mDataSource;
}
private void onRenderers(TrackRenderer[] renderers) {
@@ -347,9 +346,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* Releases the player.
*/
public void release() {
- if (mMediaDataSource != null) {
- mSourceManager.releaseDataSource(mMediaDataSource);
- mMediaDataSource = null;
+ if (mDataSource != null) {
+ mSourceManager.releaseDataSource(mDataSource);
+ mDataSource = null;
}
if (mBuilderCallback != null) {
mBuilderCallback.cancel();
@@ -479,6 +478,23 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
+ * Returns the number of tracks exposed by the specified renderer.
+ */
+ public int getTrackCount(int rendererIndex) {
+ return mPlayer.getTrackCount(rendererIndex);
+ }
+
+ /**
+ * Selects a track for the specified renderer.
+ */
+ public void setSelectedTrack(int rendererIndex, int trackIndex) {
+ if (trackIndex >= getTrackCount(rendererIndex)) {
+ return;
+ }
+ mPlayer.setSelectedTrack(rendererIndex, trackIndex);
+ }
+
+ /**
* Gets the main handler of the player.
*/
/* package */ Handler getMainHandler() {
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index f6ea84d9..0e46c9cf 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -17,10 +17,10 @@
package com.android.tv.tuner.exoplayer;
import android.content.Context;
-import android.media.MediaDataSource;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.upstream.DataSource;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer;
@@ -43,7 +43,7 @@ public class MpegTsRendererBuilder implements RendererBuilder {
}
@Override
- public void buildRenderers(MpegTsPlayer mpegTsPlayer, MediaDataSource dataSource,
+ public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource,
RendererBuilderCallback callback) {
// Build the video and audio renderers.
SampleExtractor extractor = dataSource == null ?
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
index aaf4a391..7bf116c8 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java
@@ -16,17 +16,15 @@
package com.android.tv.tuner.exoplayer;
-import android.media.MediaDataSource;
+import android.net.Uri;
import android.os.Handler;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
-import com.google.android.exoplayer.MediaFormatUtil;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.MimeTypes;
-import com.android.tv.tuner.TunerFlags;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.SamplePool;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
@@ -38,7 +36,7 @@ import java.util.LinkedList;
import java.util.List;
/**
- * Extracts samples from {@link MediaDataSource} for MPEG-TS streams.
+ * Extracts samples from {@link DataSource} for MPEG-TS streams.
*/
public final class MpegTsSampleExtractor implements SampleExtractor {
public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
@@ -64,22 +62,17 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
}
/**
- * Creates MpegTsSampleExtractor for {@link MediaDataSource}.
+ * Creates MpegTsSampleExtractor for {@link DataSource}.
*
- * @param source the {@link MediaDataSource} to extract from
+ * @param source the {@link DataSource} to extract from
* @param bufferManager the manager for reading & writing samples backed by physical storage
* @param bufferListener the {@link PlaybackBufferListener}
* to notify buffer storage status change
*/
- public MpegTsSampleExtractor(MediaDataSource source,
- BufferManager bufferManager, PlaybackBufferListener bufferListener) {
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- mSampleExtractor = new ExoPlayerSampleExtractor(new DataSourceAdapter(source),
- bufferManager, bufferListener, false);
- } else {
- mSampleExtractor = new FrameworkSampleExtractor(source, bufferManager, bufferListener,
- false);
- }
+ public MpegTsSampleExtractor(DataSource source, BufferManager bufferManager,
+ PlaybackBufferListener bufferListener) {
+ mSampleExtractor = new ExoPlayerSampleExtractor(Uri.EMPTY, source, bufferManager,
+ bufferListener, false);
init();
}
@@ -97,6 +90,13 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
}
@Override
+ public void maybeThrowError() throws IOException {
+ if (mSampleExtractor != null) {
+ mSampleExtractor.maybeThrowError();
+ }
+ }
+
+ @Override
public boolean prepare() throws IOException {
if(!mSampleExtractor.prepare()) {
return false;
@@ -124,8 +124,8 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
mCea708TextTrackIndex = trackCount;
}
if (mCea708TextTrackIndex >= 0) {
- mTrackFormats.add(MediaFormatUtil.createTextMediaFormat(MIMETYPE_TEXT_CEA_708,
- mTrackFormats.get(0).durationUs));
+ mTrackFormats.add(MediaFormat.createTextFormat(null, MIMETYPE_TEXT_CEA_708, 0,
+ mTrackFormats.get(0).durationUs, ""));
}
return true;
}
@@ -224,81 +224,112 @@ public final class MpegTsSampleExtractor implements SampleExtractor {
public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { }
private abstract class CcParser {
+ // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using
+ // relatively small buffer size in order to minimize memory footprint increase.
+ protected final byte[] mBuffer = new byte[1024];
+
abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs);
- protected void parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) {
+ protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) {
// For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9.
int pos = offset;
if (pos + 2 >= buffer.position()) {
- return;
+ return offset;
}
boolean processCcDataFlag = (buffer.get(pos) & 64) != 0;
int ccCount = buffer.get(pos) & 0x1f;
pos += 2;
if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) {
- return;
+ return offset;
}
SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES);
for (int i = 0; i < 3 * ccCount; i++) {
- holder.data.put(buffer.get(pos + i));
+ holder.data.put(buffer.get(pos++));
}
holder.timeUs = presentationTimeUs;
mPendingCcSamples.add(holder);
+ return pos;
}
}
private class Mpeg2CcParser extends CcParser {
+ private static final int PATTERN_LENGTH = 9;
+
@Override
public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
- int pos = 0;
- while (pos + 9 < buffer.position()) {
- // Find the start prefix code of private user data.
- if (buffer.get(pos) == 0
- && buffer.get(pos + 1) == 0
- && buffer.get(pos + 2) == 1
- && (buffer.get(pos + 3) & 0xff) == 0xb2) {
- // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user
- // identifier and user data type code 3.
- if (buffer.get(pos + 4) == 'G'
- && buffer.get(pos + 5) == 'A'
- && buffer.get(pos + 6) == '9'
- && buffer.get(pos + 7) == '4'
- && buffer.get(pos + 8) == 3) {
- parseClosedCaption(buffer, pos + 9, presentationTimeUs);
+ int totalSize = buffer.position();
+ // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
+ // overlapping to handle the case that the pattern exists in the boundary.
+ for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
+ buffer.position(i);
+ int size = Math.min(totalSize - i, mBuffer.length);
+ buffer.get(mBuffer, 0, size);
+ int j = 0;
+ while (j < size - PATTERN_LENGTH) {
+ // Find the start prefix code of private user data.
+ if (mBuffer[j] == 0
+ && mBuffer[j + 1] == 0
+ && mBuffer[j + 2] == 1
+ && (mBuffer[j + 3] & 0xff) == 0xb2) {
+ // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user
+ // identifier and user data type code 3.
+ if (mBuffer[j + 4] == 'G'
+ && mBuffer[j + 5] == 'A'
+ && mBuffer[j + 6] == '9'
+ && mBuffer[j + 7] == '4'
+ && mBuffer[j + 8] == 3) {
+ j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH,
+ presentationTimeUs) - i;
+ } else {
+ j += PATTERN_LENGTH;
+ }
+ } else {
+ ++j;
}
- pos += 9;
- } else {
- ++pos;
}
}
+ buffer.position(totalSize);
}
}
private class H264CcParser extends CcParser {
+ private static final int PATTERN_LENGTH = 14;
+
@Override
public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) {
- int pos = 0;
- while (pos + 7 < buffer.position()) {
- // Find the start prefix code of a NAL Unit.
- if (buffer.get(pos) == 0
- && buffer.get(pos + 1) == 0
- && buffer.get(pos + 2) == 1) {
- int nalType = buffer.get(pos + 3) & 0x1f;
- int payloadType = buffer.get(pos + 4) & 0xff;
-
- // ATSC closed caption data embedded in H264 private user data has NAL type 6,
- // payload type 4, and 'GA94' user identifier for ATSC.
- if (nalType == 6 && payloadType == 4 && buffer.get(pos + 9) == 'G'
- && buffer.get(pos + 10) == 'A'
- && buffer.get(pos + 11) == '9'
- && buffer.get(pos + 12) == '4') {
- parseClosedCaption(buffer, pos + 14, presentationTimeUs);
+ int totalSize = buffer.position();
+ // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with
+ // overlapping to handle the case that the pattern exists in the boundary.
+ for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) {
+ buffer.position(i);
+ int size = Math.min(totalSize - i, mBuffer.length);
+ buffer.get(mBuffer, 0, size);
+ int j = 0;
+ while (j < size - PATTERN_LENGTH) {
+ // Find the start prefix code of a NAL Unit.
+ if (mBuffer[j] == 0
+ && mBuffer[j + 1] == 0
+ && mBuffer[j + 2] == 1) {
+ int nalType = mBuffer[j + 3] & 0x1f;
+ int payloadType = mBuffer[j + 4] & 0xff;
+
+ // ATSC closed caption data embedded in H264 private user data has NAL type
+ // 6, payload type 4, and 'GA94' user identifier for ATSC.
+ if (nalType == 6 && payloadType == 4 && mBuffer[j + 9] == 'G'
+ && mBuffer[j + 10] == 'A'
+ && mBuffer[j + 11] == '9'
+ && mBuffer[j + 12] == '4') {
+ j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH,
+ presentationTimeUs) - i;
+ } else {
+ j += 7;
+ }
+ } else {
+ ++j;
}
- pos += 7;
- } else {
- ++pos;
}
}
+ buffer.position(totalSize);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java
index 45b78bd1..6007b0be 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java
@@ -154,6 +154,9 @@ public final class MpegTsSampleSource implements SampleSource, SampleSourceReade
if (mPreparationError != null) {
throw mPreparationError;
}
+ if (mSampleExtractor != null) {
+ mSampleExtractor.maybeThrowError();
+ }
}
@Override
diff --git a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java
index 47790ccb..543588c7 100644
--- a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java
@@ -41,10 +41,17 @@ import java.util.List;
public interface SampleExtractor {
/**
+ * If the extractor is currently having difficulty preparing or loading samples, then this
+ * method throws the underlying error. Otherwise does nothing.
+ *
+ * @throws IOException The underlying error.
+ */
+ void maybeThrowError() throws IOException;
+
+ /**
* Prepares the extractor for reading track metadata and samples.
*
- * @return whether the source is ready; if {@code false}, {@link #prepare()} must be called
- * again
+ * @return whether the source is ready; if {@code false}, this method must be called again.
* @throws IOException thrown if the source can't be read
*/
boolean prepare() throws IOException;
@@ -109,12 +116,12 @@ public interface SampleExtractor {
* @param listener the OnCompletionListener
* @param handler the {@link Handler} for {@link Handler#post(Runnable)} of OnCompletionListener
*/
- public void setOnCompletionListener(OnCompletionListener listener, Handler handler);
+ void setOnCompletionListener(OnCompletionListener listener, Handler handler);
/**
* The listener for SampleExtractor being completed.
*/
- public interface OnCompletionListener {
+ interface OnCompletionListener {
/**
* Called when sample extraction is completed.
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
index 434b46af..9dae2e34 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
@@ -37,6 +37,7 @@ import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
/**
* Decodes and renders AC3 audio.
@@ -105,6 +106,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private long mInterpolatedTimeUs;
private long mPreviousPositionUs;
private boolean mIsStopped;
+ private ArrayList<Integer> mTracksIndex;
public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler,
EventListener listener) {
@@ -120,6 +122,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
mCodecCounters = new CodecCounters();
mMonitor = new AudioTrackMonitor();
mAudioClock = new AudioClock();
+ mTracksIndex = new ArrayList<>();
}
@Override
@@ -139,8 +142,10 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
}
for (int i = 0; i < mSource.getTrackCount(); i++) {
if (handlesMimeType(mSource.getFormat(i).mimeType)) {
- mTrackIndex = i;
- return true;
+ if (mTrackIndex < 0) {
+ mTrackIndex = i;
+ }
+ mTracksIndex.add(i);
}
}
@@ -150,18 +155,19 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
@Override
protected int getTrackCount() {
- return mTrackIndex < 0 ? 0 : 1;
+ return mTracksIndex.size();
}
@Override
protected MediaFormat getFormat(int track) {
- Assertions.checkArgument(mTrackIndex != -1 && track == 0);
- return mSource.getFormat(mTrackIndex);
+ Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
+ return mSource.getFormat(mTracksIndex.get(track));
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining) {
- Assertions.checkArgument(mTrackIndex != -1 && track == 0);
+ Assertions.checkArgument(track >= 0 && track < mTracksIndex.size());
+ mTrackIndex = mTracksIndex.get(track);
mSource.enable(mTrackIndex, positionUs);
seekToInternal(positionUs);
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
index 4c46021f..2bf86b5a 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
@@ -16,17 +16,12 @@
package com.android.tv.tuner.exoplayer.ac3;
-import android.media.MediaCodec;
import android.os.Handler;
-import android.util.Log;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecSelector;
import com.google.android.exoplayer.SampleSource;
-import com.android.tv.tuner.TunerFlags;
-
-import java.nio.ByteBuffer;
/**
* MPEG-2 TS audio track renderer.
@@ -40,10 +35,6 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
private final String TAG = "Ac3TrackRenderer";
private final boolean DEBUG = false;
- private int mZeroPresentationTimeCount;
- private Long mPresentationTimeOffset;
- private boolean mPresentationTimeOffsetNeeded;
- private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
private final Ac3EventListener mListener;
public interface Ac3EventListener extends EventListener {
@@ -63,72 +54,6 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
}
@Override
- protected void onDiscontinuity(long positionUs) throws ExoPlaybackException {
- super.onDiscontinuity(positionUs);
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- return;
- }
- if (DEBUG) Log.d(TAG, "onDiscontinuity(), positionUs = " + positionUs);
- mZeroPresentationTimeCount = 0;
- mPresentationTimeOffset = null;
- mPresentationTimeOffsetNeeded = false;
- }
-
- @Override
- protected void onQueuedInputBuffer(long presentationTimeUs, ByteBuffer buffer,
- int bufferSize, boolean sampleEncrypted) {
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- return;
- }
- if (DEBUG) Log.d(TAG, "onQueuedInputBuffer(), presentationTimeUs = " + presentationTimeUs);
- if (presentationTimeUs == 0) {
- // A sequence of consecutive zero presentation times indicate
- // the starting of a data stream.
- // Count the number of leading zeros
- mZeroPresentationTimeCount++;
- // Start waiting for the first non-zero presentation time.
- mPresentationTimeOffsetNeeded = true;
- } else if (mPresentationTimeOffset == null && mPresentationTimeOffsetNeeded) {
- // Sets time offset based on the first non-zero presentation timestamp,
- // which is the first timestamp we can trust.
- mPresentationTimeOffset = mZeroPresentationTimeCount
- * Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US
- - presentationTimeUs;
- }
-
- }
-
- @Override
- protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
- ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex,
- boolean shouldSkip) throws ExoPlaybackException {
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer,
- bufferInfo, bufferIndex, shouldSkip);
- }
- if (mPresentationTimeOffset != null) {
- // Adjust the presentation time. We don't modify the given {@code bufferInfo} here since
- // this method can be called multiple times with the same buffer.
- long presentationTimeUs =
- Math.max(bufferInfo.presentationTimeUs - mPresentationTimeOffset, 0);
- mBufferInfo.set(bufferInfo.offset, bufferInfo.size,
- presentationTimeUs, bufferInfo.flags);
- } else {
- mBufferInfo.set(bufferInfo.offset, bufferInfo.size,
- bufferInfo.presentationTimeUs, bufferInfo.flags);
- }
- try {
- return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer,
- mBufferInfo, bufferIndex, shouldSkip);
- } catch (IllegalArgumentException e) {
- if (isAudioTrackSetPlaybackParamsError(e)) {
- notifyAudioTrackSetPlaybackParamsError(e);
- }
- return false;
- }
- }
-
- @Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
if (messageType == MSG_SET_PLAYBACK_PARAMS) {
try {
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index 8cf80420..eb596e93 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -81,6 +81,7 @@ public class BufferManager {
private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
private long mTotalWriteSize;
private long mTotalWriteTimeNs;
+ private float mWriteBandwidth = 0.0f;
private volatile int mSpeedCheckCount;
private boolean mDisabled = false;
@@ -555,7 +556,8 @@ public class BufferManager {
}
}
- private void resetWriteStat() {
+ private void resetWriteStat(float writeBandwidth) {
+ mWriteBandwidth = writeBandwidth;
mTotalWriteSize = 0;
mTotalWriteTimeNs = 0;
}
@@ -585,8 +587,8 @@ public class BufferManager {
return false;
}
mSpeedCheckCount++;
- float megabytePerSecond = getWriteBandwidth();
- resetWriteStat();
+ float megabytePerSecond = calculateWriteBandwidth();
+ resetWriteStat(megabytePerSecond);
if (DEBUG) {
Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps");
}
@@ -594,9 +596,14 @@ public class BufferManager {
}
/**
- * Returns the disk write speed in megabytes per second.
+ * Returns recent write bandwidth in MBps. If recent bandwidth is not available,
+ * returns {float -1.0f}.
*/
- private float getWriteBandwidth() {
+ public float getWriteBandwidth() {
+ return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth;
+ }
+
+ private float calculateWriteBandwidth() {
if (mTotalWriteTimeNs == 0) {
return -1;
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index f9efa0de..6a0502a7 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -61,8 +61,11 @@ public class DvrStorageManager implements BufferManager.StorageManager {
@Override
public void clearStorage() {
if (mIsRecording) {
- for (File file : mBufferDir.listFiles()) {
- file.delete();
+ File[] files = mBufferDir.listFiles();
+ if (files != null && files.length > 0) {
+ for (File file : files) {
+ file.delete();
+ }
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index 9243c568..4869b49f 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -183,12 +183,16 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
@Override
public void handleWriteSpeedSlow() throws IOException{
- Log.w(TAG, "Disk is too slow for recording");
if (mBufferReason == BUFFER_REASON_RECORDING) {
- // Stops the recording immediately.
- throw new IOException("Write bandwidth is not enough");
+ // Recording does not need to stop because I/O speed is slow temporarily.
+ // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS.
+ // Reaching EoS will stop recording eventually.
+ Log.w(TAG, "Disk I/O speed is slow for recording temporarily: "
+ + mBufferManager.getWriteBandwidth() + "MBps");
+ return;
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
+ Log.w(TAG, "Disk is too slow for trickplay");
mBufferManager.disable();
mBufferListener.onDiskTooSlow();
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index d8276059..37ae4022 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -27,6 +27,7 @@ import android.util.Pair;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.util.MimeTypes;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import java.io.IOException;
@@ -65,6 +66,7 @@ public class SampleChunkIoHelper implements Handler.Callback {
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
private long mBufferDurationUs = 0;
+ private boolean mWriteEnded;
private boolean mErrorNotified;
private boolean mFinished;
@@ -150,6 +152,7 @@ public class SampleChunkIoHelper implements Handler.Callback {
for (int i = 0; i < mTrackCount; ++i) {
mBufferManager.loadTrackFromStorage(mIds.get(i), mSamplePool);
}
+ mWriteEnded = true;
} else {
for (int i = 0; i < mTrackCount; ++i) {
mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i));
@@ -289,6 +292,12 @@ public class SampleChunkIoHelper implements Handler.Callback {
int index = params.index;
mIoHandler.removeMessages(MSG_READ, index);
SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs);
+ if (chunk == null) {
+ String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs
+ + "is not found";
+ SoftPreconditions.checkNotNull(chunk, TAG, errorMessage);
+ throw new IOException(errorMessage);
+ }
mReadIoStates[index].openRead(chunk);
if (mHandlerReadSampleBuffers[index] != null) {
SampleHolder sample;
@@ -314,6 +323,11 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS);
} else {
if (mReadIoStates[index].isReadFinished()) {
+ for (int i = 0; i < mTrackCount; ++i) {
+ if (!mReadIoStates[i].isReadFinished()) {
+ return;
+ }
+ }
mIoCallback.onIoReachedEos();
return;
}
@@ -332,6 +346,10 @@ public class SampleChunkIoHelper implements Handler.Callback {
private void doWrite(IoParams params) throws IOException {
try {
+ if (mWriteEnded) {
+ SoftPreconditions.checkState(false);
+ return;
+ }
int index = params.index;
SampleHolder sample = params.sample;
SampleChunk nextChunk = null;
@@ -355,6 +373,10 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doCloseWrite() throws IOException {
+ if (mWriteEnded) {
+ return;
+ }
+ mWriteEnded = true;
boolean readFinished = true;
for (int i = 0; i < mTrackCount; ++i) {
readFinished = readFinished && mReadIoStates[i].isReadFinished();
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index 9051b73b..258a5cd0 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -18,6 +18,8 @@ package com.android.tv.tuner.exoplayer.buffer;
import android.content.Context;
import android.media.MediaFormat;
+import android.os.AsyncTask;
+import android.os.Looper;
import android.provider.Settings;
import android.util.Pair;
@@ -64,8 +66,24 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public void clearStorage() {
- for (File file : mBufferDir.listFiles()) {
- file.delete();
+ File files[] = mBufferDir.listFiles();
+ if (files == null || files.length == 0) {
+ return;
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ for (File file : files) {
+ file.delete();
+ }
+ return null;
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ for (File file : files) {
+ file.delete();
+ }
}
}
diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java
index b286cff9..4b3ffe40 100644
--- a/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -46,7 +46,7 @@ import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsipData;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.source.FileTsStreamer;
-import com.android.tv.tuner.source.TsMediaDataSource;
+import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsStreamer;
import com.android.tv.tuner.source.TunerTsStreamer;
import com.android.tv.tuner.tvinput.ChannelDataManager;
@@ -120,6 +120,7 @@ public class ScanFragment extends SetupFragment {
}
});
Bundle args = getArguments();
+ // TODO: Handle the case when the fragment is restored.
startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
if (TunerInputInfoUtils.isBuiltInTuner(getActivity())){
@@ -147,8 +148,10 @@ public class ScanFragment extends SetupFragment {
@Override
public void onDetach() {
- // Ensure scan task will stop.
- mChannelScanTask.stopScan();
+ if (mChannelScanTask != null) {
+ // Ensure scan task will stop.
+ mChannelScanTask.stopScan();
+ }
super.onDetach();
}
@@ -231,7 +234,7 @@ public class ScanFragment extends SetupFragment {
}
private class ChannelScanTask extends AsyncTask<Void, Integer, Void>
- implements EventDetector.EventListener {
+ implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener {
private static final int MAX_PROGRESS = 100;
private final Activity mActivity;
@@ -260,6 +263,7 @@ public class ScanFragment extends SetupFragment {
}
mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this) : null;
mConditionStopped = new ConditionVariable();
+ mChannelDataManager.setChannelScanListener(this, new Handler());
}
private void maybeSetChannelListVisible() {
@@ -302,28 +306,10 @@ public class ScanFragment extends SetupFragment {
mScanChannelList.addAll(ChannelScanFileParser.parseScanFile(
getResources().openRawResource(mChannelMapId)));
scanChannels();
- mChannelDataManager.setCurrentVersion(mActivity);
- mChannelDataManager.release();
return null;
}
@Override
- protected void onPostExecute(Void result) {
- mIsFinished = true;
- TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(),
- mChannelDataManager.getScannedChannelCount());
- // Cancel a previously shown recommendation card.
- TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext());
- // Mark scan as done
- TunerPreferences.setScanDone(mActivity.getApplicationContext());
- // finishing will be done manually.
- if (mFinishingProgressDialog != null) {
- mFinishingProgressDialog.dismiss();
- }
- onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
- }
-
- @Override
protected void onCancelled() {
SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel");
}
@@ -404,7 +390,7 @@ public class ScanFragment extends SetupFragment {
tunerChannel.getProgramNumber()));
tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber);
tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber());
- mChannelDataManager.notifyChannelDetected(tunerChannel, true);
+ onChannelDetected(tunerChannel, true);
}
}
}
@@ -452,6 +438,25 @@ public class ScanFragment extends SetupFragment {
getString(R.string.ut_setup_cancel), true, false);
}
}
+
+ @Override
+ public void onChannelHandlingDone() {
+ mChannelDataManager.setCurrentVersion(mActivity);
+ mChannelDataManager.releaseSafely();
+ mIsFinished = true;
+ TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(),
+ mChannelDataManager.getScannedChannelCount());
+ // Cancel a previously shown recommendation card.
+ TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext());
+ // Mark scan as done
+ TunerPreferences.setScanDone(mActivity.getApplicationContext());
+ // finishing will be done manually.
+ if (mFinishingProgressDialog != null) {
+ mFinishingProgressDialog.dismiss();
+ }
+ onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ mChannelScanTask = null;
+ }
}
private static class FakeTsStreamer implements TsStreamer {
@@ -493,7 +498,7 @@ public class ScanFragment extends SetupFragment {
}
@Override
- public TsMediaDataSource createMediaDataSource() {
+ public TsDataSource createDataSource() {
return null;
}
}
diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java
index 757eeddd..068543cd 100644
--- a/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -38,12 +38,6 @@ public class ScanResultFragment extends SetupMultiPaneFragment {
public static final String ACTION_CATEGORY =
"com.android.tv.tuner.setup.ScanResultFragment";
- /**
- * An action which moves to previous page when the user presses BACK button.
- * In some cases, more than one page can be popped out.
- */
- public static final int ACTION_BACK_TO_CONNECTION_TYPE = ACTION_DONE - 1;
-
@Override
protected SetupGuidedStepFragment onCreateContentFragment() {
return new ContentFragment();
diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
index 8359ce73..78121bc5 100644
--- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java
+++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
@@ -31,7 +31,9 @@ import android.graphics.BitmapFactory;
import android.media.tv.TvContract;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
+import android.util.Log;
import android.view.KeyEvent;
+import android.widget.Toast;
import com.android.tv.TvApplication;
import com.android.tv.common.TvCommonConstants;
@@ -40,6 +42,7 @@ import com.android.tv.common.ui.setup.SetupActivity;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import com.android.tv.tuner.util.TunerInputInfoUtils;
@@ -48,6 +51,7 @@ import com.android.tv.tuner.util.TunerInputInfoUtils;
* An activity that serves tuner setup process.
*/
public class TunerSetupActivity extends SetupActivity {
+ private final String TAG = "TunerSetupActivity";
// For the recommendation card
private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity";
private static final String NOTIFY_TAG = "TunerSetup";
@@ -107,9 +111,23 @@ public class TunerSetupActivity extends SetupActivity {
}
return true;
case ConnectionTypeFragment.ACTION_CATEGORY:
+ TunerHal hal = TunerHal.createInstance(getApplicationContext());
+ if (hal == null) {
+ finish();
+ Toast.makeText(getApplicationContext(),
+ R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show();
+ return true;
+ }
+ try {
+ hal.close();
+ } catch (Exception e) {
+ Log.e(TAG, "Tuner hal close failed", e);
+ return true;
+ }
mLastScanFragment = new ScanFragment();
Bundle args = new Bundle();
- args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]);
+ args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE,
+ CHANNEL_MAP_SCAN_FILE[actionId]);
mLastScanFragment.setArguments(args);
showFragment(mLastScanFragment, true);
return true;
diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java
index 617440bc..14997ee4 100644
--- a/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -20,9 +20,10 @@ import android.os.Environment;
import android.util.Log;
import android.util.SparseBooleanArray;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.DataSpec;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.ChannelScanFileParser.ScanChannel;
-import com.android.tv.tuner.TunerFlags;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.ts.TsParser;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -67,31 +68,17 @@ public class FileTsStreamer implements TsStreamer {
private Thread mStreamingThread;
private StreamProvider mSource;
- public static class FileMediaDataSource extends TsMediaDataSource {
+ public static class FileDataSource extends TsDataSource {
private final FileTsStreamer mTsStreamer;
private final AtomicLong mLastReadPosition = new AtomicLong(0);
private long mStartBufferedPosition;
- private FileMediaDataSource(FileTsStreamer tsStreamer) {
+ private FileDataSource(FileTsStreamer tsStreamer) {
mTsStreamer = tsStreamer;
mStartBufferedPosition = tsStreamer.getBufferedPosition();
}
@Override
- public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
- int ret = mTsStreamer.readAt(mStartBufferedPosition + pos, buffer, offset, amount);
- if (ret > 0) {
- mLastReadPosition.set(pos + ret);
- }
- return ret;
- }
-
- @Override
- public long getSize() {
- return -1L;
- }
-
- @Override
public long getBufferedPosition() {
return mTsStreamer.getBufferedPosition() - mStartBufferedPosition;
}
@@ -108,10 +95,25 @@ public class FileTsStreamer implements TsStreamer {
mStartBufferedPosition += offset;
}
- // This will be called by {@link MediaExtractor}
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ mLastReadPosition.set(0);
+ return C.LENGTH_UNBOUNDED;
+ }
+
@Override
public void close() {
}
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer,
+ offset, readLength);
+ if (ret > 0) {
+ mLastReadPosition.addAndGet(ret);
+ }
+ return ret;
+ }
}
/**
@@ -154,13 +156,8 @@ public class FileTsStreamer implements TsStreamer {
}
mEventDetector.start(mSource, channel.getProgramNumber());
mSource.addPidFilter(channel.getVideoPid());
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- // ExoPlayer's extractor expects all the tracks which belong to the channel.
- for (Integer i : channel.getAudioPids()) {
- mSource.addPidFilter(i);
- }
- } else {
- mSource.addPidFilter(channel.getAudioPid());
+ for (Integer i : channel.getAudioPids()) {
+ mSource.addPidFilter(i);
}
mSource.addPidFilter(channel.getPcrPid());
mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID);
@@ -199,8 +196,8 @@ public class FileTsStreamer implements TsStreamer {
}
@Override
- public TsMediaDataSource createMediaDataSource() {
- return new FileMediaDataSource(this);
+ public TsDataSource createDataSource() {
+ return new FileDataSource(this);
}
/**
diff --git a/src/com/android/tv/tuner/source/TsMediaDataSource.java b/src/com/android/tv/tuner/source/TsDataSource.java
index 1bef78fe..2ce3e670 100644
--- a/src/com/android/tv/tuner/source/TsMediaDataSource.java
+++ b/src/com/android/tv/tuner/source/TsDataSource.java
@@ -16,12 +16,12 @@
package com.android.tv.tuner.source;
-import android.media.MediaDataSource;
+import com.google.android.exoplayer.upstream.DataSource;
/**
- * {@link MediaDatasource} for MPEG-TS stream, which will be used by {@link MediaExtractor}.
+ * {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}.
*/
-public abstract class TsMediaDataSource extends MediaDataSource {
+public abstract class TsDataSource implements DataSource {
/**
* Returns the number of bytes being buffered by {@link TsStreamer} so far.
@@ -33,7 +33,7 @@ public abstract class TsMediaDataSource extends MediaDataSource {
}
/**
- * Returns the offset position where the last {@link MediaDataSource#readAt} read.
+ * Returns the offset position where the last {@link DataSource#read} read.
*
* @return the last read position
*/
diff --git a/src/com/android/tv/tuner/source/TsMediaDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java
index d6c3c3b1..7286cd8c 100644
--- a/src/com/android/tv/tuner/source/TsMediaDataSourceManager.java
+++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -17,7 +17,6 @@
package com.android.tv.tuner.source;
import android.content.Context;
-import android.media.MediaDataSource;
import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.TunerChannel;
@@ -27,15 +26,15 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
- * Manages {@link TsMediaDataSource} for playback and recording.
+ * Manages {@link DataSource} for playback and recording.
* The class hides handling of {@link TunerHal} and {@link TsStreamer} from other classes.
- * One TsMediaDataSourceManager should be created for per session.
+ * One TsDataSourceManager should be created for per session.
*/
-public class TsMediaDataSourceManager {
- private static String TAG = "TsMediaDataSourceManager";
+public class TsDataSourceManager {
+ private static String TAG = "TsDataSourceManager";
private static final Object sLock = new Object();
- private static final Map<TsMediaDataSource, TsStreamer> sTsStreamers =
+ private static final Map<TsDataSource, TsStreamer> sTsStreamers =
new ConcurrentHashMap<>();
private static int sSequenceId;
@@ -48,33 +47,33 @@ public class TsMediaDataSourceManager {
private boolean mKeepTuneStatus;
/**
- * Creates TsMediaDataSourceManager to create and release {@link MediaDataSource} which will be
+ * Creates TsDataSourceManager to create and release {@link DataSource} which will be
* used for playing and recording.
* @param isRecording {@code true} when for recording, {@code false} otherwise
- * @return {@link TsMediaDataSourceManager}
+ * @return {@link TsDataSourceManager}
*/
- public static TsMediaDataSourceManager createSourceManager(boolean isRecording) {
+ public static TsDataSourceManager createSourceManager(boolean isRecording) {
int id;
synchronized (sLock) {
id = ++sSequenceId;
}
- return new TsMediaDataSourceManager(id, isRecording);
+ return new TsDataSourceManager(id, isRecording);
}
- private TsMediaDataSourceManager(int id, boolean isRecording) {
+ private TsDataSourceManager(int id, boolean isRecording) {
mId = id;
mIsRecording = isRecording;
mKeepTuneStatus = true;
}
/**
- * Creates or retrieves {@link TsMediaDataSource} for playing or recording
+ * Creates or retrieves {@link TsDataSource} for playing or recording
* @param context a {@link Context} instance
* @param channel to play or record
* @param eventListener for program information which will be scanned from MPEG2-TS stream
- * @return {@link TsMediaDataSource} which will provide the specified channel stream
+ * @return {@link TsDataSource} which will provide the specified channel stream
*/
- public TsMediaDataSource createDataSource(Context context, TunerChannel channel,
+ public TsDataSource createDataSource(Context context, TunerChannel channel,
EventDetector.EventListener eventListener) {
if (channel.getType() == Channel.TYPE_FILE) {
// MPEG2 TS captured stream file recording is not supported.
@@ -83,7 +82,7 @@ public class TsMediaDataSourceManager {
}
FileTsStreamer streamer = new FileTsStreamer(eventListener);
if (streamer.startStream(channel)) {
- TsMediaDataSource source = streamer.createMediaDataSource();
+ TsDataSource source = streamer.createDataSource();
sTsStreamers.put(source, streamer);
return source;
}
@@ -94,14 +93,14 @@ public class TsMediaDataSourceManager {
}
/**
- * Releases the specified {@link MediaDataSource} and underlying {@link TunerHal}.
+ * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}.
* @param source to release
*/
- public void releaseDataSource(TsMediaDataSource source) {
- if (source instanceof TunerTsStreamer.TunerMediaDataSource) {
+ public void releaseDataSource(TsDataSource source) {
+ if (source instanceof TunerTsStreamer.TunerDataSource) {
mTunerStreamerManager.releaseDataSource(
source, mId, !mIsRecording && mKeepTuneStatus);
- } else if (source instanceof FileTsStreamer.FileMediaDataSource) {
+ } else if (source instanceof FileTsStreamer.FileDataSource) {
FileTsStreamer streamer = (FileTsStreamer) sTsStreamers.get(source);
if (streamer != null) {
sTsStreamers.remove(source);
diff --git a/src/com/android/tv/tuner/source/TsStreamWriter.java b/src/com/android/tv/tuner/source/TsStreamWriter.java
new file mode 100644
index 00000000..30650555
--- /dev/null
+++ b/src/com/android/tv/tuner/source/TsStreamWriter.java
@@ -0,0 +1,237 @@
+/*
+ * 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.tuner.source;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.tv.tuner.data.TunerChannel;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Stores TS files to the disk for debugging.
+ */
+public class TsStreamWriter {
+ private static final String TAG = "TsStreamWriter";
+ private static final boolean DEBUG = false;
+
+ private static final long TIME_LIMIT_MS = 10000; // 10s
+ private static final int NO_INSTANCE_ID = 0;
+ private static final int MAX_GET_ID_RETRY_COUNT = 5;
+ private static final int MAX_INSTANCE_ID = 10000;
+ private static final String SEPARATOR = "_";
+
+ private FileOutputStream mFileOutputStream;
+ private long mFileStartTimeMs;
+ private String mFileName = null;
+ private final String mDirectoryPath;
+ private final File mDirectory;
+ private final int mInstanceId;
+ private TunerChannel mChannel;
+
+ public TsStreamWriter(Context context) {
+ File externalFilesDir = context.getExternalFilesDir(null);
+ if (externalFilesDir == null || !externalFilesDir.isDirectory()) {
+ mDirectoryPath = null;
+ mDirectory = null;
+ mInstanceId = NO_INSTANCE_ID;
+ if (DEBUG) {
+ Log.w(TAG, "Fail to get external files dir!");
+ }
+ } else {
+ mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream";
+ mDirectory = new File(mDirectoryPath);
+ if (!mDirectory.exists()) {
+ boolean madeDir = mDirectory.mkdir();
+ if (!madeDir) {
+ Log.w(TAG, "Error. Fail to create folder!");
+ }
+ }
+ mInstanceId = generateInstanceId();
+ }
+ }
+
+ /**
+ * Sets the current channel.
+ *
+ * @param channel curren channel of the stream
+ */
+ public void setChannel(TunerChannel channel) {
+ mChannel = channel;
+ }
+
+ /**
+ * Opens a file to store TS data.
+ */
+ public void openFile() {
+ if (mChannel == null || mDirectoryPath == null) {
+ return;
+ }
+ mFileStartTimeMs = System.currentTimeMillis();
+ mFileName = mChannel.getDisplayNumber() + SEPARATOR + mFileStartTimeMs + SEPARATOR
+ + mInstanceId + ".ts";
+ String filePath = mDirectoryPath + "/" + mFileName;
+ try {
+ mFileOutputStream = new FileOutputStream(filePath, false);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Cannot open file: " + filePath, e);
+ }
+ }
+
+ /**
+ * Closes the file and stops storing TS data.
+ *
+ * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped
+ * {@code false} otherwise
+ */
+ public void closeFile(boolean calledWhenStopStream) {
+ if (mFileOutputStream == null) {
+ return;
+ }
+ try {
+ mFileOutputStream.close();
+ deleteOutdatedFiles(calledWhenStopStream);
+ mFileName = null;
+ mFileOutputStream = null;
+ } catch (IOException e) {
+ Log.w(TAG, "Error on closing file.", e);
+ }
+ }
+
+ /**
+ * Writes the data to the file.
+ *
+ * @param buffer the data to be written
+ * @param bytesWritten number of bytes written
+ */
+ public void writeToFile(byte[] buffer, int bytesWritten) {
+ if (mFileOutputStream == null) {
+ return;
+ }
+ if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) {
+ closeFile(false);
+ openFile();
+ }
+ try {
+ mFileOutputStream.write(buffer, 0, bytesWritten);
+ } catch (IOException e) {
+ Log.w(TAG, "Error on writing TS stream.", e);
+ }
+ }
+
+ /**
+ * Deletes outdated files to save storage.
+ *
+ * @param deleteAll {@code true} if all the files with the relative ID should be deleted
+ * {@code false} if the most recent file should not be deleted
+ */
+ private void deleteOutdatedFiles(boolean deleteAll) {
+ if (mFileName == null) {
+ return;
+ }
+ if (mDirectory == null || !mDirectory.isDirectory()) {
+ Log.e(TAG, "Error. The folder doesn't exist!");
+ return;
+ }
+ if (mFileName == null) {
+ Log.e(TAG, "Error. The current file name is null!");
+ return;
+ }
+ for (File file : mDirectory.listFiles()) {
+ if (file.isFile() && getFileId(file) == mInstanceId
+ && (deleteAll || !mFileName.equals(file.getName()))) {
+ boolean deleted = file.delete();
+ if (DEBUG && !deleted) {
+ Log.w(TAG, "Failed to delete " + file.getName());
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates a unique instance ID.
+ *
+ * @return a unique instance ID
+ */
+ private int generateInstanceId() {
+ if (mDirectory == null) {
+ return NO_INSTANCE_ID;
+ }
+ Set<Integer> idSet = getExistingIds();
+ if (idSet == null) {
+ return NO_INSTANCE_ID;
+ }
+ for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) {
+ // Range [1, MAX_INSTANCE_ID]
+ int id = (int)Math.floor(Math.random() * MAX_INSTANCE_ID) + 1;
+ if (!idSet.contains(id)) {
+ return id;
+ }
+ }
+ return NO_INSTANCE_ID;
+ }
+
+ /**
+ * Gets all existing instance IDs.
+ *
+ * @return a set of all existing instance IDs
+ */
+ private Set<Integer> getExistingIds() {
+ if (mDirectory == null || !mDirectory.isDirectory()) {
+ return null;
+ }
+
+ Set<Integer> idSet = new HashSet<>();
+ for (File file : mDirectory.listFiles()) {
+ int id = getFileId(file);
+ if(id != NO_INSTANCE_ID) {
+ idSet.add(id);
+ }
+ }
+ return idSet;
+ }
+
+ /**
+ * Gets the instance ID of a given file.
+ *
+ * @param file the file whose TsStreamWriter ID is returned
+ * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available
+ */
+ private static int getFileId(File file) {
+ if (file == null || !file.isFile()) {
+ return NO_INSTANCE_ID;
+ }
+ String fileName = file.getName();
+ int lastSeparator = fileName.lastIndexOf(SEPARATOR);
+ if (!fileName.endsWith(".ts") || lastSeparator == -1) {
+ return NO_INSTANCE_ID;
+ }
+ try {
+ return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3));
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Log.e(TAG, fileName + " is not a valid file name.");
+ }
+ }
+ return NO_INSTANCE_ID;
+ }
+}
diff --git a/src/com/android/tv/tuner/source/TsStreamer.java b/src/com/android/tv/tuner/source/TsStreamer.java
index cc28fc44..1ac950bb 100644
--- a/src/com/android/tv/tuner/source/TsStreamer.java
+++ b/src/com/android/tv/tuner/source/TsStreamer.java
@@ -46,11 +46,11 @@ public interface TsStreamer {
void stopStream();
/**
- * Creates {@link TsMediaDataSource} which will provide MPEG-2 TS stream for
+ * Creates {@link TsDataSource} which will provide MPEG-2 TS stream for
* {@link android.media.MediaExtractor}. The source will start from the position
* where it is created.
*
- * @return {@link TsMediaDataSource}
+ * @return {@link TsDataSource}
*/
- TsMediaDataSource createMediaDataSource();
+ TsDataSource createDataSource();
}
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java
index c756c381..b24048e6 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -16,11 +16,15 @@
package com.android.tv.tuner.source;
+import android.content.Context;
import android.util.Log;
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.DataSpec;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.ChannelScanFileParser;
import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.tvinput.EventDetector;
import com.android.tv.tuner.tvinput.EventDetector.EventListener;
@@ -42,11 +46,6 @@ public class TunerTsStreamer implements TsStreamer {
private static final int READ_TIMEOUT_MS = 5000; // 5 secs.
private static final int BUFFER_UNDERRUN_SLEEP_MS = 10;
- private static final int BUFFER_KEY_VERSION = 1;
-
- // UTCK stands for USB Tuner Buffer Key.
- private static final String BUFFER_KEY_PREFIX = "UTCK";
-
private final Object mCircularBufferMonitor = new Object();
private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE];
private long mBytesFetched;
@@ -59,31 +58,19 @@ public class TunerTsStreamer implements TsStreamer {
private Thread mStreamingThread;
private final EventDetector mEventDetector;
- public static class TunerMediaDataSource extends TsMediaDataSource {
+ private final TsStreamWriter mTsStreamWriter;
+
+ public static class TunerDataSource extends TsDataSource {
private final TunerTsStreamer mTsStreamer;
private final AtomicLong mLastReadPosition = new AtomicLong(0);
private long mStartBufferedPosition;
- private TunerMediaDataSource(TunerTsStreamer tsStreamer) {
+ private TunerDataSource(TunerTsStreamer tsStreamer) {
mTsStreamer = tsStreamer;
mStartBufferedPosition = tsStreamer.getBufferedPosition();
}
@Override
- public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
- int ret = mTsStreamer.readAt(mStartBufferedPosition + pos, buffer, offset, amount);
- if (ret > 0) {
- mLastReadPosition.set(pos + ret);
- }
- return ret;
- }
-
- @Override
- public long getSize() {
- return -1L;
- }
-
- @Override
public long getBufferedPosition() {
return mTsStreamer.getBufferedPosition() - mStartBufferedPosition;
}
@@ -100,19 +87,40 @@ public class TunerTsStreamer implements TsStreamer {
mStartBufferedPosition += offset;
}
- // This will be called by {@link MediaExtractor}
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ mLastReadPosition.set(0);
+ return C.LENGTH_UNBOUNDED;
+ }
+
@Override
public void close() {
}
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength) throws IOException {
+ int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer,
+ offset, readLength);
+ if (ret > 0) {
+ mLastReadPosition.addAndGet(ret);
+ }
+ return ret;
+ }
}
/**
* Creates {@link TsStreamer} for playing or recording the specified channel.
* @param tunerHal the HAL for tuner device
* @param eventListener the listener for channel & program information
*/
- public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) {
+ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
mTunerHal = tunerHal;
mEventDetector = new EventDetector(mTunerHal, eventListener);
+ mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ?
+ new TsStreamWriter(context) : null;
+ }
+
+ public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) {
+ this(tunerHal, eventListener, null);
}
@Override
@@ -122,9 +130,16 @@ public class TunerTsStreamer implements TsStreamer {
mTunerHal.addPidFilter(channel.getVideoPid(),
TunerHal.FILTER_TYPE_VIDEO);
}
- if (channel.hasAudio()) {
- mTunerHal.addPidFilter(channel.getAudioPid(),
- TunerHal.FILTER_TYPE_AUDIO);
+ boolean audioFilterSet = false;
+ for (Integer audioPid : channel.getAudioPids()) {
+ if (!audioFilterSet) {
+ mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO);
+ audioFilterSet = true;
+ } else {
+ // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use
+ // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks.
+ mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER);
+ }
}
mTunerHal.addPidFilter(channel.getPcrPid(),
TunerHal.FILTER_TYPE_PCR);
@@ -143,6 +158,10 @@ public class TunerTsStreamer implements TsStreamer {
mLastReadPosition.set(0L);
mEndOfStreamSent = false;
}
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.setChannel(mChannel);
+ mTsStreamWriter.openFile();
+ }
mStreamingThread = new StreamingThread();
mStreamingThread.start();
Log.i(TAG, "Streaming started");
@@ -193,11 +212,15 @@ public class TunerTsStreamer implements TsStreamer {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.closeFile(true);
+ mTsStreamWriter.setChannel(null);
+ }
}
@Override
- public TsMediaDataSource createMediaDataSource() {
- return new TunerMediaDataSource(this);
+ public TsDataSource createDataSource() {
+ return new TunerDataSource(this);
}
/**
@@ -260,6 +283,10 @@ public class TunerTsStreamer implements TsStreamer {
continue;
}
+ if (mTsStreamWriter != null) {
+ mTsStreamWriter.writeToFile(dataBuffer, bytesWritten);
+ }
+
if (mEventDetector != null) {
mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten);
}
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
index bd3fa5d6..cf1f6dcf 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -33,7 +33,7 @@ import java.util.Set;
/**
* Manages {@link TunerTsStreamer} for playback and recording.
* The class hides handling of {@link TunerHal} from other classes.
- * This class is used by {@link TsMediaDataSourceManager}. Don't use this class directly.
+ * This class is used by {@link TsDataSourceManager}. Don't use this class directly.
*/
class TunerTsStreamerManager {
// The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator
@@ -42,7 +42,7 @@ class TunerTsStreamerManager {
private final Object mCancelLock = new Object();
private final StreamerFinder mStreamerFinder = new StreamerFinder();
private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
- private final Map<TsMediaDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
+ private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
private final TunerHalManager mTunerHalManager = new TunerHalManager();
private static TunerTsStreamerManager sInstance;
@@ -59,7 +59,7 @@ class TunerTsStreamerManager {
private TunerTsStreamerManager() { }
- synchronized TsMediaDataSource createDataSource(
+ synchronized TsDataSource createDataSource(
Context context, TunerChannel channel, EventDetector.EventListener listener,
int sessionId, boolean reuse) {
TsStreamerCreator creator;
@@ -67,7 +67,7 @@ class TunerTsStreamerManager {
if (mStreamerFinder.containsLocked(channel)) {
mStreamerFinder.appendSessionLocked(channel, sessionId);
TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel);
- TsMediaDataSource source = streamer.createMediaDataSource();
+ TsDataSource source = streamer.createDataSource();
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -82,7 +82,7 @@ class TunerTsStreamerManager {
}
if (!creator.isCancelledLocked()) {
mStreamerFinder.putLocked(channel, sessionId, streamer);
- TsMediaDataSource source = streamer.createMediaDataSource();
+ TsDataSource source = streamer.createDataSource();
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -95,7 +95,7 @@ class TunerTsStreamerManager {
return null;
}
- synchronized void releaseDataSource(TsMediaDataSource source, int sessionId,
+ synchronized void releaseDataSource(TsDataSource source, int sessionId,
boolean reuse) {
TunerTsStreamer streamer;
synchronized (mCancelLock) {
@@ -203,7 +203,7 @@ class TunerTsStreamerManager {
}
}
if (!canceled) {
- TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener);
+ TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext);
if (tsStreamer.startStream(mChannel)) {
return tsStreamer;
}
diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java
index f47d48c9..5d3e728a 100644
--- a/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/src/com/android/tv/tuner/ts/SectionParser.java
@@ -532,6 +532,13 @@ public class SectionParser {
int numChannelsInSection = (data[9] & 0xff);
int sectionNumber = (data[6] & 0xff);
int lastSectionNumber = (data[7] & 0xff);
+ if (sectionNumber > lastSectionNumber) {
+ // According to section 6.3.1 of the spec ATSC A/65,
+ // last section number is the largest section number.
+ Log.w(TAG, "Invalid VCT. Section Number " + sectionNumber + " > Last Section Number "
+ + lastSectionNumber);
+ return false;
+ }
int pos = 10;
List<VctItem> results = new ArrayList<>();
for (int i = 0; i < numChannelsInSection; ++i) {
diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
index f8573732..a16bc522 100644
--- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -39,6 +39,8 @@ import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.ConvertUtils;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -46,7 +48,6 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -76,8 +77,7 @@ public class ChannelDataManager implements Handler.Callback {
private static final int MSG_BUILD_CHANNEL_MAP = 3;
private static final int MSG_REQUEST_PROGRAMS = 4;
private static final int MSG_CLEAR_CHANNELS = 6;
- private static final int MSG_SCAN_COMPLETED = 7;
- private static final int MSG_CHECK_VERSION = 8;
+ private static final int MSG_CHECK_VERSION = 7;
// Throttle the batch operations to avoid TransactionTooLargeException.
private static final int BATCH_OPERATION_COUNT = 100;
@@ -97,6 +97,8 @@ public class ChannelDataManager implements Handler.Callback {
private final Context mContext;
private final String mInputId;
private ProgramInfoListener mListener;
+ private ChannelScanListener mChannelScanListener;
+ private Handler mChannelScanHandler;
private final HandlerThread mHandlerThread;
private final Handler mHandler;
private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap;
@@ -107,7 +109,7 @@ public class ChannelDataManager implements Handler.Callback {
private final ConcurrentSkipListSet<TunerChannel> mScannedChannels;
private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels;
private final AtomicBoolean mIsScanning;
- private CountDownLatch mScanLatch;
+ private final AtomicBoolean scanCompleted = new AtomicBoolean();
public interface ProgramInfoListener {
@@ -135,6 +137,13 @@ public class ChannelDataManager implements Handler.Callback {
void onRescanNeeded();
}
+ public interface ChannelScanListener {
+ /**
+ * Invoked when all pending channels have been handled.
+ */
+ void onChannelHandlingDone();
+ }
+
public ChannelDataManager(Context context) {
mContext = context;
mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(),
@@ -176,11 +185,20 @@ public class ChannelDataManager implements Handler.Callback {
mListener = listener;
}
+ public void setChannelScanListener(ChannelScanListener listener, Handler handler) {
+ mChannelScanListener = listener;
+ mChannelScanHandler = handler;
+ }
+
public void release() {
mHandler.removeCallbacksAndMessages(null);
mHandlerThread.quitSafely();
}
+ public void releaseSafely() {
+ mHandlerThread.quitSafely();
+ }
+
public TunerChannel getChannel(long channelId) {
TunerChannel channel = mTunerChannelMap.get(channelId);
if (channel != null) {
@@ -215,7 +233,13 @@ public class ChannelDataManager implements Handler.Callback {
}
public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
- mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
+ if (mIsScanning.get()) {
+ // During scanning, channels should be handle first to improve scan time.
+ // EIT items can be handled in background after channel scan.
+ mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel));
+ } else {
+ mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget();
+ }
}
// For scanning process
@@ -249,26 +273,35 @@ public class ChannelDataManager implements Handler.Callback {
* obsolete channels, which are previously scanned but are not in the current scanned result.
*/
public void notifyScanCompleted() {
- mScanLatch = new CountDownLatch(1);
- mHandler.sendEmptyMessage(MSG_SCAN_COMPLETED);
- try {
- mScanLatch.await();
- } catch (InterruptedException e) {
- Log.e(TAG, "Scanning process could not finish", e);
- }
+ // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue
+ // and avoid race conditions.
+ scanCompleted.set(true);
+ mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null));
+ }
+
+ public void scannedChannelHandlingCompleted() {
mIsScanning.set(false);
- if (mPreviousScannedChannels.isEmpty()) {
- return;
- }
- ArrayList<ContentProviderOperation> ops = new ArrayList<>();
- for (TunerChannel channel : mPreviousScannedChannels) {
- ops.add(ContentProviderOperation.newDelete(
- TvContract.buildChannelUri(channel.getChannelId())).build());
+ if (!mPreviousScannedChannels.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (TunerChannel channel : mPreviousScannedChannels) {
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildChannelUri(channel.getChannelId())).build());
+ }
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Error deleting obsolete channels", e);
+ }
}
- try {
- mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
- } catch (RemoteException | OperationApplicationException e) {
- Log.e(TAG, "Error deleting obsolete channels", e);
+ if (mChannelScanListener != null && mChannelScanHandler != null) {
+ mChannelScanHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mChannelScanListener.onChannelHandlingDone();
+ }
+ });
+ } else {
+ Log.e(TAG, "Error. mChannelScanListener is null.");
}
}
@@ -296,7 +329,14 @@ public class ChannelDataManager implements Handler.Callback {
}
case MSG_HANDLE_CHANNEL: {
TunerChannel channel = (TunerChannel) msg.obj;
- handleChannel(channel);
+ if (channel != null) {
+ handleChannel(channel);
+ }
+ if (scanCompleted.get() && mIsScanning.get()
+ && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) {
+ // Complete the scan when all found channels have already been handled.
+ scannedChannelHandlingCompleted();
+ }
return true;
}
case MSG_BUILD_CHANNEL_MAP: {
@@ -318,10 +358,6 @@ public class ChannelDataManager implements Handler.Callback {
clearChannels();
return true;
}
- case MSG_SCAN_COMPLETED: {
- mScanLatch.countDown();
- return true;
- }
case MSG_CHECK_VERSION: {
checkVersion();
return true;
@@ -347,15 +383,69 @@ public class ChannelDataManager implements Handler.Callback {
long currentTime = System.currentTimeMillis();
List<EitItem> oldItems = getAllProgramsForChannel(channel, currentTime,
currentTime + PROGRAM_QUERY_DURATION);
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// TODO: Find a right way to check if the programs are added outside.
+ boolean addedOutside = false;
for (EitItem item : oldItems) {
if (item.getEventId() == 0) {
- // The event has been added outside TV tuner. Do not update programs.
- return;
+ // The event has been added outside TV tuner.
+ addedOutside = true;
+ break;
}
}
+
+ // Inserting programs only when there is no overlapping with existing data assuming that:
+ // 1. external EPG is more accurate and rich and
+ // 2. the data we add here will be updated when we apply external EPG.
+ if (addedOutside) {
+ // oldItemCount cannot be 0 if addedOutside is true.
+ int oldItemCount = oldItems.size();
+ for (EitItem newItem : items) {
+ if (newItem.getEndTimeUtcMillis() < currentTime) {
+ continue;
+ }
+ long newItemStartTime = newItem.getStartTimeUtcMillis();
+ long newItemEndTime = newItem.getEndTimeUtcMillis();
+ if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) {
+ // Start time smaller than that of any old items. Insert if no overlap.
+ if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue;
+ } else if (newItemStartTime
+ > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) {
+ // Start time larger than that of any old item. Insert if no overlap.
+ if (newItemStartTime
+ < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis()) continue;
+ } else {
+ int pos = Collections.binarySearch(oldItems, newItem,
+ new Comparator<EitItem>() {
+ @Override
+ public int compare(EitItem lhs, EitItem rhs) {
+ return Long.compare(lhs.getStartTimeUtcMillis(),
+ rhs.getStartTimeUtcMillis());
+ }
+ });
+ if (pos >= 0) {
+ // Same start Time found. Overlapped.
+ continue;
+ }
+ int insertPoint = -1 - pos;
+ // Check the two adjacent items.
+ if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis()
+ || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) {
+ continue;
+ }
+ }
+ ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
+ TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId()));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+ applyBatch(channel.getName(), ops);
+ return;
+ }
+
List<EitItem> outdatedOldItems = new ArrayList<>();
- ArrayList<ContentProviderOperation> ops = new ArrayList<>();
Map<Integer, EitItem> newEitItemMap = new HashMap<>();
for (EitItem item : items) {
newEitItemMap.put(item.getEventId(), item);
@@ -379,22 +469,8 @@ public class ChannelDataManager implements Handler.Callback {
item.setDescription(oldItem.getDescription());
}
if (item.compareTo(oldItem) != 0) {
- ops.add(ContentProviderOperation.newUpdate(
- TvContract.buildProgramUri(oldItem.getProgramId()))
- .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
- .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
- item.getStartTimeUtcMillis())
- .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
- item.getEndTimeUtcMillis())
- .withValue(TvContract.Programs.COLUMN_CONTENT_RATING,
- item.getContentRating())
- .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE,
- item.getAudioLanguage())
- .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
- item.getDescription())
- .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER,
- item.getEventId())
- .build());
+ ops.add(buildContentProviderOperation(ContentProviderOperation.newUpdate(
+ TvContract.buildProgramUri(oldItem.getProgramId())), item, null));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
@@ -428,9 +504,24 @@ public class ChannelDataManager implements Handler.Callback {
if (item.getEndTimeUtcMillis() < currentTime) {
continue;
}
- ops.add(ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI)
- .withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId())
- .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
+ ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
+ TvContract.Programs.CONTENT_URI), item, channel.getChannelId()));
+ if (ops.size() >= BATCH_OPERATION_COUNT) {
+ applyBatch(channel.getName(), ops);
+ ops.clear();
+ }
+ }
+
+ applyBatch(channel.getName(), ops);
+ }
+
+ private ContentProviderOperation buildContentProviderOperation(
+ ContentProviderOperation.Builder builder, EitItem item, Long channelId) {
+ if (channelId != null) {
+ builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId);
+ }
+ if (item != null) {
+ builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
.withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
item.getStartTimeUtcMillis())
.withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
@@ -442,15 +533,9 @@ public class ChannelDataManager implements Handler.Callback {
.withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
item.getDescription())
.withValue(TvContract.Programs.COLUMN_VERSION_NUMBER,
- item.getEventId())
- .build());
- if (ops.size() >= BATCH_OPERATION_COUNT) {
- applyBatch(channel.getName(), ops);
- ops.clear();
- }
+ item.getEventId());
}
-
- applyBatch(channel.getName(), ops);
+ return builder.build();
}
private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) {
diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java
index e62df62f..27bbb8c7 100644
--- a/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/EventDetector.java
@@ -20,7 +20,6 @@ import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
-import com.android.tv.tuner.TunerFlags;
import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.data.Track.AtscAudioTrack;
import com.android.tv.tuner.data.Track.AtscCaptionTrack;
@@ -39,7 +38,6 @@ import java.util.Set;
*/
public class EventDetector {
private static final String TAG = "EventDetector";
- private static final String CABLE_MODULATION = "QAM";
private static final boolean DEBUG = false;
public static final int ALL_PROGRAM_NUMBERS = -1;
@@ -62,12 +60,7 @@ public class EventDetector {
@Override
public void onPatDetected(List<PsiData.PatItem> items) {
for (PsiData.PatItem i : items) {
- // In case of tuning to a cable channel, we don't add filters for EPG update for
- // other channels due to b/29490412. We do the same when extractor in ExoPlayer is
- // used since it doesn't allow PMTs not related to the channel to play.
- if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()
- || (!mModulation.startsWith(CABLE_MODULATION)
- && !TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER)) {
+ if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) {
mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER);
}
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index 352687c1..6ec55e4f 100644
--- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -24,6 +24,7 @@ import android.database.Cursor;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
@@ -32,23 +33,20 @@ import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;
-import com.google.android.exoplayer.upstream.DataSource;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.recording.RecordingCapability;
+import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.RecordedProgram;
import com.android.tv.tuner.DvbDeviceAccessor;
-import com.android.tv.tuner.TunerFlags;
import com.android.tv.tuner.data.PsipData;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.exoplayer.DataSourceAdapter;
import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
-import com.android.tv.tuner.exoplayer.FrameworkSampleExtractor;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
-import com.android.tv.tuner.source.TsMediaDataSource;
-import com.android.tv.tuner.source.TsMediaDataSourceManager;
-import com.android.tv.util.DvrTunerStorageUtils;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.util.Utils;
import java.io.File;
@@ -75,11 +73,13 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long PREPARE_RECORDER_POLL_MS = 50;
private static final int MSG_TUNE = 1;
private static final int MSG_START_RECORDING = 2;
- private static final int MSG_STOP_RECORDING = 3;
- private static final int MSG_MONITOR_STORAGE_STATUS = 4;
- private static final int MSG_RELEASE = 5;
+ private static final int MSG_PREPARE_RECODER = 3;
+ private static final int MSG_STOP_RECORDING = 4;
+ private static final int MSG_MONITOR_STORAGE_STATUS = 5;
+ private static final int MSG_RELEASE = 6;
private final RecordingCapability mCapabilities;
public RecordingCapability getCapabilities() {
@@ -97,12 +97,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
+ private final DvrStorageStatusManager mDvrStorageStatusManager;
private final Handler mHandler;
- private final TsMediaDataSourceManager mSourceManager;
+ private final TsDataSourceManager mSourceManager;
private final Random mRandom = new Random();
- private final CountDownLatch mReleaseLatch = new CountDownLatch(1);
- private TsMediaDataSource mTunerSource;
+ private TsDataSource mTunerSource;
private TunerChannel mChannel;
private File mStorageDir;
private long mRecordStartTime;
@@ -122,9 +122,11 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
HandlerThread handlerThread = new HandlerThread(TAG);
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), this);
+ mDvrStorageStatusManager =
+ TvApplication.getSingletons(context).getDvrStorageStatusManager();
mChannelDataManager = dataManager;
mChannelDataManager.checkDataVersion(context);
- mSourceManager = TsMediaDataSourceManager.createSourceManager(true);
+ mSourceManager = TsDataSourceManager.createSourceManager(true);
mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
mInputId = inputId;
if (DEBUG) Log.d(TAG, mCapabilities.toString());
@@ -202,13 +204,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
public void release() {
mHandler.removeCallbacksAndMessages(null);
mHandler.sendEmptyMessage(MSG_RELEASE);
- try {
- mReleaseLatch.await();
- } catch (InterruptedException e) {
- Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e);
- } finally {
- mHandler.getLooper().quitSafely();
- }
}
@Override
@@ -216,6 +211,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
switch (msg.what) {
case MSG_TUNE: {
Uri channelUri = (Uri) msg.obj;
+ if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
if (doTune(channelUri)) {
mSession.onTuned(channelUri);
} else {
@@ -224,12 +220,31 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_START_RECORDING: {
+ if (DEBUG) Log.d(TAG, "Start recording");
if (!doStartRecording((Uri) msg.obj)) {
reset();
}
return true;
}
+ case MSG_PREPARE_RECODER: {
+ if (DEBUG) Log.d(TAG, "Preparing recorder");
+ if (!mRecorderRunning) {
+ return true;
+ }
+ try {
+ if (!mRecorder.prepare()) {
+ mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER,
+ PREPARE_RECORDER_POLL_MS);
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor");
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ reset();
+ }
+ return true;
+ }
case MSG_STOP_RECORDING: {
+ if (DEBUG) Log.d(TAG, "Stop recording");
if (mSessionState != STATE_RECORDING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
reset();
@@ -244,12 +259,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
if (mSessionState != STATE_RECORDING) {
return true;
}
- if (!DvrTunerStorageUtils.isStorageSufficient(mContext)) {
+ if (!mDvrStorageStatusManager.isStorageSufficient()) {
if (mRecorderRunning) {
stopRecorder();
}
+ new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
- Utils.deleteDirOrFile(mStorageDir);
reset();
} else {
mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS,
@@ -262,7 +277,8 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
// without notification.
reset();
mSourceManager.release();
- mReleaseLatch.countDown();
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.getLooper().quitSafely();
return true;
}
}
@@ -318,7 +334,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
return false;
}
- if (!DvrTunerStorageUtils.isStorageSufficient(mContext)) {
+ if (!mDvrStorageStatusManager.isStorageSufficient()) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
Log.w(TAG, "Tuning failed due to insufficient storage.");
return false;
@@ -339,8 +355,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Log.e(TAG, "Recording session status abnormal");
return false;
}
- mStorageDir = DvrTunerStorageUtils.isStorageSufficient(mContext) ?
- DvrTunerStorageUtils.getRecordingDataDirectory(mContext, getStorageKey()) : null;
+ mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ?
+ new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(),
+ getStorageKey()) : null;
if (mStorageDir == null) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
Log.w(TAG, "Failed to start recording due to insufficient storage.");
@@ -350,23 +367,13 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true));
mRecordStartTime = System.currentTimeMillis();
- if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) {
- mRecorder = new ExoPlayerSampleExtractor(new DataSourceAdapter(mTunerSource),
- mBufferManager, this, true);
- } else {
- mRecorder = new FrameworkSampleExtractor(mTunerSource, mBufferManager, this, true);
- }
+ mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this,
+ true);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
- try {
- mRecorder.prepare();
- } catch (IOException e) {
- Log.w(TAG, "Failed to start recording. Couldn't prepare a extractor");
- mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
- return false;
- }
mSessionState = STATE_RECORDING;
mRecorderRunning = true;
+ mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS,
STORAGE_MONITOR_INTERVAL_MS);
@@ -553,9 +560,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
if (!success && lastExtractedPositionUs <
TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
+ new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Recording failed during recording");
- Utils.deleteDirOrFile(mStorageDir);
return;
}
Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
@@ -563,11 +570,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime,
mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs));
if (uri == null) {
+ new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Inserting a recording to DB failed");
- Utils.deleteDirOrFile(mStorageDir);
return;
}
mSession.onRecordFinished(uri);
}
+
+ private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
+
+ @Override
+ public Void doInBackground(File... files) {
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for(File file : files) {
+ Utils.deleteDirOrFile(file);
+ }
+ return null;
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java
index 1494e212..abfd2b30 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -44,6 +44,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.util.GlobalSettingsUtils;
import com.android.tv.tuner.util.StatusTextUtils;
import com.android.tv.tuner.util.SystemPropertiesProxy;
@@ -97,7 +98,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
mAudioStatusView.setVisibility(View.INVISIBLE);
mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
- context.getString(R.string.ut_ac3_passthrough_unavailable))));
+ context.getString(R.string.ut_surround_sound_disabled))));
CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
mSessionWorker = new TunerSessionWorker(context, channelDataManager,
@@ -267,7 +268,14 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
return true;
}
case MSG_UI_SHOW_AUDIO_UNPLAYABLE: {
- mAudioStatusView.setVisibility(View.VISIBLE);
+ // Showing message of enabling surround sound only when global surround sound
+ // setting is "never".
+ final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
+ if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
+ mAudioStatusView.setVisibility(View.VISIBLE);
+ } else {
+ Log.e(TAG, "Audio is unavailable, surround sound setting is " + value);
+ }
return true;
}
case MSG_UI_HIDE_AUDIO_UNPLAYABLE: {
diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index ab9dda50..c0a613a4 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -45,6 +45,7 @@ import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.ExoPlayer;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsipData.EitItem;
@@ -56,8 +57,8 @@ import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
-import com.android.tv.tuner.source.TsMediaDataSource;
-import com.android.tv.tuner.source.TsMediaDataSourceManager;
+import com.android.tv.tuner.source.TsDataSource;
+import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.util.StatusTextUtils;
import java.io.File;
@@ -112,6 +113,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private static final int MSG_BUFFER_STATE_CHANGED = 1021;
private static final int MSG_PROGRAM_DATA_RESULT = 1022;
private static final int MSG_STOP_TUNE = 1023;
+ private static final int MSG_SET_SURFACE = 1024;
+ private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
private static final int TS_PACKET_SIZE = 188;
private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
@@ -121,9 +124,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
+ // The following 3s is defined empirically. This should be larger than 2s considering video
+ // key frame interval in the TS stream.
private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
private static final int PLAYBACK_RETRY_DELAY_MS = 5000;
- private static final int MAX_RETRY_COUNT = 5;
+ private static final int MAX_IMMEDIATE_RETRY_COUNT = 5;
private static final long INVALID_TIME = -1;
// Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
@@ -145,7 +150,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
- private final TsMediaDataSourceManager mSourceManager;
+ private final TsDataSourceManager mSourceManager;
private volatile Surface mSurface;
private volatile float mVolume = 1.0f;
private volatile boolean mCaptionEnabled;
@@ -179,6 +184,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
private final TunerSession mSession;
private int mPlayerState = ExoPlayer.STATE_IDLE;
+ private long mPreparingStartTimeMs;
private long mBufferingStartTimeMs;
private long mReadyStartTimeMs;
@@ -196,7 +202,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mChannelDataManager = channelDataManager;
mChannelDataManager.setListener(this);
mChannelDataManager.checkDataVersion(mContext);
- mSourceManager = TsMediaDataSourceManager.createSourceManager(false);
+ mSourceManager = TsDataSourceManager.createSourceManager(false);
mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
mTvTracks = new ArrayList<>();
mAudioTrackMap = new SparseArray<>();
@@ -206,6 +212,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mCaptionEnabled = captioningManager.isEnabled();
mPlaybackParams.setSpeed(1.0f);
mBufferManager = bufferManager;
+ mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
}
@@ -236,7 +243,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// mSurface is kept even when tune is called right after. But, messages can be deleted by
// tune or updateChannelBlockStatus. So mSurface should be stored here, not through message.
mSurface = surface;
- mHandler.sendEmptyMessage(MSG_RESET_PLAYBACK);
+ mHandler.sendEmptyMessage(MSG_SET_SURFACE);
}
/**
@@ -355,6 +362,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return;
}
mReadyStartTimeMs = INVALID_TIME;
+ mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
if (playbackState == ExoPlayer.STATE_READY) {
if (DEBUG) Log.d(TAG, "ExoPlayer ready");
@@ -362,6 +370,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
sendMessage(MSG_START_PLAYBACK, mPlayer);
}
mReadyStartTimeMs = SystemClock.elapsedRealtime();
+ } else if (playbackState == ExoPlayer.STATE_PREPARING) {
+ mPreparingStartTimeMs = SystemClock.elapsedRealtime();
} else if (playbackState == ExoPlayer.STATE_BUFFERING) {
mBufferingStartTimeMs = SystemClock.elapsedRealtime();
} else if (playbackState == ExoPlayer.STATE_ENDED) {
@@ -377,6 +387,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@Override
public void onError(Exception e) {
+ if (TunerPreferences.getStoreTsStream(mContext)) {
+ // Crash intentionally to capture the error causing TS file.
+ Log.e(TAG, "Crash intentionally to capture the error causing TS file. "
+ + e.getMessage());
+ SoftPreconditions.checkState(false);
+ }
// There maybe some errors that finally raise ExoPlaybackException and will be handled here.
// If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
// retrying playback is not helpful.
@@ -401,7 +417,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
mBufferStartTimeMs = mRecordStartTimeMs =
(mRecordingId != null) ? 0 : System.currentTimeMillis();
- mSession.notifyVideoAvailable();
+ notifyVideoAvailable();
mReportedDrawnToSurface = true;
// If surface is drawn successfully, it means that the playback was brought back
@@ -415,6 +431,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
} else {
stopCaptionTrack();
}
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
}
}
@@ -584,6 +601,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mHandler.hasMessages(MSG_TUNE)) {
return true;
}
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
Uri channelUri = (Uri) msg.obj;
String recording = null;
long channelId = parseChannel(channelUri);
@@ -595,8 +613,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (channel == null && recording == null) {
Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
stopTune();
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return true;
}
mHandler.removeCallbacksAndMessages(null);
@@ -619,8 +636,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
stopPlayback();
stopCaptionTrack();
resetTvTracks();
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return true;
}
case MSG_RELEASE: {
@@ -642,7 +658,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) {
Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
}
- if (mRetryCount <= MAX_RETRY_COUNT) {
+ if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
resetPlayback();
} else {
// When it reaches this point, it may be due to an error that occurred in
@@ -651,11 +667,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
stopPlayback();
stopCaptionTrack();
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
- // After MAX_RETRY_COUNT, give some delay of an empirically chosen value
- // before recovering the playback.
+ // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically chosen
+ // value before recovering the playback.
mHandler.sendEmptyMessageDelayed(MSG_RESET_PLAYBACK,
RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
}
@@ -742,10 +757,16 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_TRICKPLAY_BY_SEEK: {
+ if (mPlayer == null) {
+ return true;
+ }
doTrickplayBySeek(msg.arg1);
return true;
}
case MSG_SMOOTH_TRICKPLAY_MONITOR: {
+ if (mPlayer == null) {
+ return true;
+ }
long systemCurrentTime = System.currentTimeMillis();
long position = getCurrentPosition();
if (mRecordingId == null) {
@@ -811,21 +832,33 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
case MSG_TIMESHIFT_PAUSE: {
if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
+ if (mPlayer == null) {
+ return true;
+ }
doTimeShiftPause();
return true;
}
case MSG_TIMESHIFT_RESUME: {
if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME");
+ if (mPlayer == null) {
+ return true;
+ }
doTimeShiftResume();
return true;
}
case MSG_TIMESHIFT_SEEK_TO: {
long position = (long) msg.obj;
if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")");
+ if (mPlayer == null) {
+ return true;
+ }
doTimeShiftSeekTo(position);
return true;
}
case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: {
+ if (mPlayer == null) {
+ return true;
+ }
doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
return true;
}
@@ -874,7 +907,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mChannel == null || mPlayer == null) {
return true;
}
- TsMediaDataSource source = mPlayer.getDataSource();
+ TsDataSource source = mPlayer.getDataSource();
long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
long positionInBytes = source != null ? source.getLastReadPosition() : 0L;
if (TunerDebug.ENABLED) {
@@ -905,16 +938,21 @@ public class TunerSessionWorker implements PlaybackBufferListener,
boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME
&& currentTime - mBufferingStartTimeMs
> PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isPreparingTooLong = mPreparingStartTimeMs != INVALID_TIME
+ && currentTime - mPreparingStartTimeMs
+ > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
boolean isWeakSignal = source != null
&& mChannel.getType() == Channel.TYPE_TUNER
- && (noBufferRead || isBufferingTooLong);
+ && (noBufferRead || isBufferingTooLong || isPreparingTooLong);
if (isWeakSignal && !mReportedWeakSignal) {
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(
+ MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS);
+ }
if (mPlayer != null) {
mPlayer.setAudioTrack(false);
}
- mReportedWeakSignal = true;
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
} else if (!isWeakSignal && mReportedWeakSignal) {
boolean isPlaybackStable = mReadyStartTimeMs != INVALID_TIME
&& currentTime - mReadyStartTimeMs
@@ -922,9 +960,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (!isPlaybackStable) {
// Wait until playback becomes stable.
} else if (mReportedDrawnToSurface) {
- mSession.notifyVideoAvailable();
+ mHandler.removeMessages(MSG_RETRY_PLAYBACK);
+ notifyVideoAvailable();
mPlayer.setAudioTrack(true);
- mReportedWeakSignal = false;
}
}
mLastLimitInBytes = limitInBytes;
@@ -932,6 +970,20 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
return true;
}
+ case MSG_SET_SURFACE: {
+ if (mPlayer != null) {
+ mPlayer.setSurface(mSurface);
+ } else {
+ // TODO: Since surface is dynamically set, we can remove the dependency of
+ // playback start on mSurface nullity.
+ resetPlayback();
+ }
+ return true;
+ }
+ case MSG_NOTIFY_AUDIO_TRACK_UPDATED: {
+ notifyAudioTracksUpdated();
+ return true;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
return false;
@@ -955,8 +1007,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mChannel.selectAudioTrack(audioTrack.index);
int newAudioPid = mChannel.getAudioPid();
if (oldAudioPid != newAudioPid) {
- // TODO: Implement a switching between tracks more smoothly.
- resetPlayback();
+ mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index);
}
mSession.notifyTrackSelected(type, trackId);
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
@@ -1061,33 +1112,48 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) {
Log.d(TAG, "Update AudioTracks " + audioTracks);
}
- removeTvTracks(TvTrackInfo.TYPE_AUDIO);
mAudioTrackMap.clear();
if (audioTracks != null) {
int index = 0;
for (AtscAudioTrack audioTrack : audioTracks) {
- String language = audioTrack.language;
- if (language == null && mChannel.getAudioTracks() != null
- && mChannel.getAudioTracks().size() == audioTracks.size()) {
- // If a language is not present, use a language field in PMT section parsed.
- language = mChannel.getAudioTracks().get(index).language;
- }
-
- // Save the index to the audio track.
- // Later, when a audio track is selected, Both an audio pid and its audio stream
- // type reside in the selected index position of the tuner channel's audio data.
audioTrack.index = index;
- TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
- TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + index);
- builder.setLanguage(language);
- builder.setAudioChannelCount(audioTrack.channelCount);
- builder.setAudioSampleRate(audioTrack.sampleRate);
- TvTrackInfo track = builder.build();
- mTvTracks.add(track);
mAudioTrackMap.put(index, audioTrack);
++index;
}
}
+ mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
+ }
+
+ private void notifyAudioTracksUpdated() {
+ if (mPlayer == null) {
+ // Audio tracks will be updated later once player initialization is done.
+ return;
+ }
+ int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
+ removeTvTracks(TvTrackInfo.TYPE_AUDIO);
+ for (int i = 0; i < audioTrackCount; i++) {
+ AtscAudioTrack audioTrack = mAudioTrackMap.get(i);
+ if (audioTrack == null) {
+ continue;
+ }
+ String language = audioTrack.language;
+ if (language == null && mChannel.getAudioTracks() != null
+ && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) {
+ // If a language is not present, use a language field in PMT section parsed.
+ language = mChannel.getAudioTracks().get(i).language;
+ }
+ // Save the index to the audio track.
+ // Later, when an audio track is selected, both the audio pid and its audio stream
+ // type reside in the selected index position of the tuner channel's audio data.
+ audioTrack.index = i;
+ TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
+ TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
+ builder.setLanguage(language);
+ builder.setAudioChannelCount(audioTrack.channelCount);
+ builder.setAudioSampleRate(audioTrack.sampleRate);
+ TvTrackInfo track = builder.build();
+ mTvTracks.add(track);
+ }
mSession.notifyTracksChanged(mTvTracks);
}
@@ -1170,10 +1236,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mPlaybackParams.setSpeed(1.0f);
mPlayerStarted = false;
mReportedDrawnToSurface = false;
- mReportedWeakSignal = false;
+ mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
+ mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
}
}
@@ -1186,8 +1253,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio.");
// Playbacks with video-only stream have not been tested yet.
// No video-only channel has been found.
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return;
}
if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio())
@@ -1204,11 +1270,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mPlayer.setPlayWhenReady(true);
mPlayer.setVolume(mVolume);
if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) {
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
- } else {
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
+ } else if (!mReportedWeakSignal) {
+ // Doesn't show buffering during weak signal.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
}
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
mPlayerStarted = true;
@@ -1232,15 +1297,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mSourceManager.setKeepTuneStatus(false);
player.release();
if (!mHandler.hasMessages(MSG_TUNE)) {
- if (mRetryCount < MAX_RETRY_COUNT) {
- // When prepare failed, there may be some errors related to hardware. In that
- // case, retry playback immediately may not help.
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer),
- PLAYBACK_RETRY_DELAY_MS);
- } else {
- mSession.notifyVideoUnavailable(
- TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
- }
+ // When prepare failed, there may be some errors related to hardware. In that
+ // case, retry playback immediately may not help.
+ notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer),
+ PLAYBACK_RETRY_DELAY_MS);
}
} else {
mPlayer = player;
@@ -1263,7 +1324,6 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mChannelBlocked || mSurface == null) {
return;
}
- mSession.notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
preparePlayback();
}
@@ -1506,4 +1566,18 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
>= mBufferStartTimeMs - mRecordStartTimeMs;
}
+
+ private void notifyVideoUnavailable(final int reason) {
+ mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ if (mSession != null) {
+ mSession.notifyVideoUnavailable(reason);
+ }
+ }
+
+ private void notifyVideoAvailable() {
+ mReportedWeakSignal = false;
+ if (mSession != null) {
+ mSession.notifyVideoAvailable();
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
new file mode 100644
index 00000000..e734b779
--- /dev/null
+++ b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
@@ -0,0 +1,166 @@
+/*
+ * 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.tuner.tvinput;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.util.Utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Creates {@link JobService} to clean up recorded program files which are not referenced
+ * from database.
+ */
+public class TunerStorageCleanUpService extends JobService {
+ private CleanUpStorageTask mTask;
+
+ @Override
+ public void onCreate() {
+ TvApplication.setCurrentRunningProcess(this, false);
+ super.onCreate();
+ mTask = new CleanUpStorageTask(this, this);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ return false;
+ }
+
+ /**
+ * Cleans up recorded program files which are not referenced from database.
+ * Cleaning up will be done periodically.
+ */
+ public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> {
+ private final static String[] mProjection = {
+ TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME,
+ TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI
+ };
+ private final static long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1);
+
+ private final Context mContext;
+ private final DvrStorageStatusManager mDvrStorageStatusManager;
+ private final JobService mJobService;
+ private final ContentResolver mContentResolver;
+
+ /**
+ * Creates a recurring storage cleaning task.
+ *
+ * @param context {@link Context}
+ * @param jobService {@link JobService}
+ */
+ public CleanUpStorageTask(Context context, JobService jobService) {
+ mContext = context;
+ mDvrStorageStatusManager =
+ TvApplication.getSingletons(mContext).getDvrStorageStatusManager();
+ mJobService = jobService;
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ private Set<String> getRecordedProgramsDirs() {
+ try (Cursor c = mContentResolver.query(
+ TvContract.RecordedPrograms.CONTENT_URI, mProjection, null, null, null)) {
+ if (c == null) {
+ return null;
+ }
+ Set<String> recordedProgramDirs = new HashSet<>();
+ while (c.moveToNext()) {
+ String packageName = c.getString(0);
+ String dataUriString = c.getString(1);
+ if (dataUriString == null) {
+ continue;
+ }
+ Uri dataUri = Uri.parse(dataUriString);
+ if (!Utils.isInBundledPackageSet(packageName)
+ || dataUri == null || dataUri.getPath() == null
+ || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) {
+ continue;
+ }
+ File recordedProgramDir = new File(dataUri.getPath());
+ try {
+ recordedProgramDirs.add(recordedProgramDir.getCanonicalPath());
+ } catch (IOException | SecurityException e) {
+ }
+ }
+ return recordedProgramDirs;
+ }
+ }
+
+ @Override
+ protected JobParameters[] doInBackground(JobParameters... params) {
+ if (mDvrStorageStatusManager.getDvrStorageStatus()
+ == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
+ return params;
+ }
+ File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory();
+ if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) {
+ return params;
+ }
+ Set<String> recordedProgramDirs = getRecordedProgramsDirs();
+ if (recordedProgramDirs == null) {
+ return params;
+ }
+ File[] files = dvrRecordingDir.listFiles();
+ if (files == null || files.length == 0) {
+ return params;
+ }
+ for (File recordingDir : files) {
+ try {
+ if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) {
+ long lastModified = recordingDir.lastModified();
+ long now = System.currentTimeMillis();
+ if (lastModified != 0
+ && lastModified < now - ELAPSED_MILLIS_TO_DELETE) {
+ // To prevent current recordings from being deleted,
+ // deletes recordings which was not modified for long enough time.
+ Utils.deleteDirOrFile(recordingDir);
+ }
+ }
+ } catch (IOException | SecurityException e) {
+ // would not happen
+ }
+ }
+ return params;
+ }
+
+ @Override
+ protected void onPostExecute(JobParameters[] params) {
+ for (JobParameters param : params) {
+ mJobService.jobFinished(param, false);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
index baefe61f..684ebdbd 100644
--- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
+++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
@@ -16,6 +16,8 @@
package com.android.tv.tuner.tvinput;
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.media.tv.TvContract;
@@ -25,6 +27,7 @@ import android.util.Log;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.android.tv.TvApplication;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
import com.android.tv.tuner.util.SystemPropertiesProxy;
@@ -32,6 +35,7 @@ import com.android.tv.tuner.util.SystemPropertiesProxy;
import java.util.Collections;
import java.util.Set;
import java.util.WeakHashMap;
+import java.util.concurrent.TimeUnit;
/**
* {@link TunerTvInputService} serves TV channels coming from a tuner device.
@@ -41,10 +45,10 @@ public class TunerTvInputService extends TvInputService
private static final String TAG = "TunerTvInputService";
private static final boolean DEBUG = false;
-
private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
+ private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
// WeakContainer for {@link TvInputSessionImpl}
private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>());
@@ -62,6 +66,19 @@ public class TunerTvInputService extends TvInputService
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
mAudioCapabilitiesReceiver.register();
mBufferManager = createBufferManager();
+ if (CommonFeatures.DVR.isEnabled(this)) {
+ JobScheduler jobScheduler =
+ (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ JobInfo pendingJob = jobScheduler.getPendingJob(DVR_STORAGE_CLEANUP_JOB_ID);
+ if (pendingJob != null) {
+ // storage cleaning job is already scheduled.
+ } else {
+ JobInfo job = new JobInfo.Builder(DVR_STORAGE_CLEANUP_JOB_ID,
+ new ComponentName(this, TunerStorageCleanUpService.class))
+ .setPersisted(true).setPeriodic(TimeUnit.DAYS.toMillis(1)).build();
+ jobScheduler.schedule(job);
+ }
+ }
if (mBufferManager == null) {
Log.i(TAG, "Trickplay is disabled");
} else {
diff --git a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java
new file mode 100644
index 00000000..0cefcbed
--- /dev/null
+++ b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java
@@ -0,0 +1,36 @@
+/*
+ * 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.tuner.util;
+
+import android.content.Context;
+import android.provider.Settings;
+
+/**
+ * Utility class that get information of global settings.
+ */
+public class GlobalSettingsUtils {
+ // Since global surround setting is hided, add the related variable here for checking surround
+ // sound setting when the audio is unavailable. Remove this workaround after b/31254857 fixed.
+ private static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output";
+ public static final int ENCODED_SURROUND_OUTPUT_NEVER = 1;
+
+ private GlobalSettingsUtils () { }
+
+ public static int getEncodedSurroundOutputSettings(Context context) {
+ return Settings.Global.getInt(context.getContentResolver(), ENCODED_SURROUND_OUTPUT, 0);
+ }
+}
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index eef05136..09acb36b 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -55,7 +55,7 @@ public class AppLayerTvView extends TvView {
public void onViewAdded(View child) {
if (child instanceof SurfaceView) {
// Note: See b/29118070 for detail.
- ((SurfaceView) child).setSecure(Experiments.ENABLE_DEVELOPER_FEATURES.get());
+ ((SurfaceView) child).setSecure(!Experiments.ENABLE_DEVELOPER_FEATURES.get());
}
super.onViewAdded(child);
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index cb371e76..3cf4de83 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -25,6 +25,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.graphics.Bitmap;
+import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.net.Uri;
@@ -57,6 +58,7 @@ import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.ImageLoader.ImageLoaderCallback;
@@ -92,6 +94,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
*/
public static final int LOCK_CHANNEL_INFO = 2;
+ private static final int DISPLAYED_CONTENT_RATINGS_COUNT = 3;
+
private static final String EMPTY_STRING = "";
private static Program sNoProgram;
@@ -114,6 +118,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private TextView mAspectRatioTextView;
private TextView mResolutionTextView;
private TextView mAudioChannelTextView;
+ private TextView[] mContentRatingsTextViews = new TextView[DISPLAYED_CONTENT_RATINGS_COUNT];
private TextView mProgramDescriptionTextView;
private String mProgramDescriptionText;
private View mAnchorView;
@@ -121,6 +126,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private Program mLastUpdatedProgram;
private final Handler mHandler = new Handler();
private final DvrManager mDvrManager;
+ private ContentRatingsManager mContentRatingsManager;
+ private TvContentRating mBlockingContentRating;
private int mLockType;
@@ -233,6 +240,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
} else {
mDvrManager = null;
}
+ mContentRatingsManager = TvApplication.getSingletons(getContext())
+ .getTvInputManagerHelper().getContentRatingsManager();
if (sNoProgram == null) {
sNoProgram = new Program.Builder()
@@ -284,6 +293,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio);
mResolutionTextView = (TextView) findViewById(R.id.resolution);
mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel);
+ mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0);
+ mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1);
+ mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2);
mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description);
mAnchorView = findViewById(R.id.anchor);
@@ -349,6 +361,15 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
/**
+ * Sets the content rating that blocks the current watched channel for displaying it in the
+ * channel banner.
+ */
+ public void setBlockingContentRating(TvContentRating rating) {
+ mBlockingContentRating = rating;
+ updateProgramRatings(mMainActivity.getCurrentProgram());
+ }
+
+ /**
* Update channel banner view.
*
* @param info A StreamInfo that includes stream information.
@@ -357,8 +378,11 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
public void updateViews(StreamInfo info) {
resetAnimationEffects();
Channel channel = mMainActivity.getCurrentChannel();
- if (!Objects.equals(mCurrentChannel, channel) && isShown()) {
- scheduleHide();
+ if (!Objects.equals(mCurrentChannel, channel)) {
+ mBlockingContentRating = null;
+ if (isShown()) {
+ scheduleHide();
+ }
}
mCurrentChannel = channel;
mChannelView.setVisibility(VISIBLE);
@@ -390,6 +414,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mAspectRatioTextView.setVisibility(View.GONE);
mResolutionTextView.setVisibility(View.GONE);
mAudioChannelTextView.setVisibility(View.GONE);
+ for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ mContentRatingsTextViews[i].setVisibility(View.GONE);
+ }
}
}
@@ -545,6 +572,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
updateProgramTimeInfo(program);
updateRecordingStatus(program);
+ updateProgramRatings(program);
// When the program is changed, but the previous resize animation has not ended yet,
// cancel the animation.
@@ -626,6 +654,28 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
: R.dimen.channel_banner_anchor_two_line_y);
}
+ private void updateProgramRatings(Program program) {
+ if (mBlockingContentRating != null) {
+ mContentRatingsTextViews[0].setText(
+ mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating));
+ mContentRatingsTextViews[0].setVisibility(View.VISIBLE);
+ for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ mContentRatingsTextViews[i].setVisibility(View.GONE);
+ }
+ return;
+ }
+ TvContentRating[] ratings = (program == null) ? null : program.getContentRatings();
+ for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ if (ratings == null || ratings.length <= i) {
+ mContentRatingsTextViews[i].setVisibility(View.GONE);
+ } else {
+ mContentRatingsTextViews[i].setText(
+ mContentRatingsManager.getDisplayNameForRating(ratings[i]));
+ mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
private void updateProgramTimeInfo(Program program) {
long durationMs = program.getDurationMillis();
long startTimeMs = program.getStartTimeUtcMillis();
@@ -775,4 +825,4 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
animator.addListener(mResizeAnimatorListener);
return animator;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index 44656de5..cbe459fb 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -62,7 +62,6 @@ import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
-import com.android.tv.dvr.DvrManager;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.NetworkUtils;
@@ -171,7 +170,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@BlockScreenType private int mBlockScreenType;
- private final DvrManager mDvrManager;
private final TvInputManagerHelper mInputManager;
private final ConnectivityManager mConnectivityManager;
private final InputSessionManager mInputSessionManager;
@@ -352,10 +350,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
if (CommonFeatures.DVR.isEnabled(context)) {
- mDvrManager = appSingletons.getDvrManager();
mInputSessionManager = appSingletons.getInputSessionManager();
} else {
- mDvrManager = null;
mInputSessionManager = null;
}
mInputManager = appSingletons.getTvInputManagerHelper();
@@ -764,6 +760,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
/**
* Returns currently blocked content rating. {@code null} if it's not blocked.
*/
+ @Override
public TvContentRating getBlockedContentRating() {
return mBlockedContentRating;
}
@@ -1005,6 +1002,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mute();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
+ mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setImageVisibility(false);
+ mHideScreenView.setText(null);
+ mBufferingSpinnerView.setVisibility(VISIBLE);
+ mute();
+ break;
case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setImageVisibility(false);
@@ -1035,12 +1038,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
private String getTuneConflictMessage(String inputId) {
- if (mDvrManager != null && inputId != null) {
+ if (inputId != null) {
TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- long time = mDvrManager.getEarliestRecordingEndTime(inputId);
- return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource,
- input.getTunerCount(),
- DateUtils.formatDateTime(getContext(), time, DateUtils.FORMAT_SHOW_TIME));
+ Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(inputId);
+ if (timeMs != null) {
+ return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource,
+ input.getTunerCount(),
+ DateUtils.formatDateTime(getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME));
+ }
}
return null;
}
diff --git a/src/com/android/tv/ui/ViewUtils.java b/src/com/android/tv/ui/ViewUtils.java
index 5a853dcd..ac181752 100644
--- a/src/com/android/tv/ui/ViewUtils.java
+++ b/src/com/android/tv/ui/ViewUtils.java
@@ -16,8 +16,11 @@
package com.android.tv.ui;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup.LayoutParams;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -42,4 +45,49 @@ public class ViewUtils {
Log.e(TAG, "Fail to call View.setTransitionAlpha", e);
}
}
-}
+
+ /**
+ * Creates an animator in view's height
+ * @param target the {@link view} animator performs on.
+ */
+ public static Animator createHeightAnimator(
+ final View target, int initialHeight, int targetHeight) {
+ ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (Integer) animation.getAnimatedValue();
+ if (value == 0) {
+ if (target.getVisibility() != View.GONE) {
+ target.setVisibility(View.GONE);
+ }
+ } else {
+ if (target.getVisibility() != View.VISIBLE) {
+ target.setVisibility(View.VISIBLE);
+ }
+ setLayoutHeight(target, value);
+ }
+ }
+ });
+ return animator;
+ }
+
+ /**
+ * Gets view's layout height.
+ */
+ public static int getLayoutHeight(View view) {
+ LayoutParams layoutParams = view.getLayoutParams();
+ return layoutParams.height;
+ }
+
+ /**
+ * Sets view's layout height.
+ */
+ public static void setLayoutHeight(View view, int height) {
+ LayoutParams layoutParams = view.getLayoutParams();
+ if (height != layoutParams.height) {
+ layoutParams.height = height;
+ view.setLayoutParams(layoutParams);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
index 067c6292..0d189cca 100644
--- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
@@ -18,6 +18,8 @@ package com.android.tv.ui.sidepanel;
import android.accounts.Account;
import android.app.Activity;
+import android.app.ApplicationErrorReport;
+import android.content.Intent;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.Toast;
@@ -27,6 +29,7 @@ import com.android.tv.TvApplication;
import com.android.tv.common.BuildConfig;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.experiments.Experiments;
+import com.android.tv.tuner.TunerPreferences;
import java.util.ArrayList;
import java.util.List;
@@ -59,6 +62,33 @@ public class DeveloperOptionFragment extends SideFragment {
}
});
}
+ items.add(new ActionItem(getString(R.string.dev_item_send_feedback)) {
+ @Override
+ protected void onSelected() {
+ Intent intent = new Intent(Intent.ACTION_APP_ERROR);
+ ApplicationErrorReport report = new ApplicationErrorReport();
+ report.packageName = report.processName = getContext().getPackageName();
+ report.time = System.currentTimeMillis();
+ report.type = ApplicationErrorReport.TYPE_NONE;
+ intent.putExtra(Intent.EXTRA_BUG_REPORT, report);
+ startActivityForResult(intent, 0);
+ }
+ });
+ items.add(new SwitchItem(getString(R.string.dev_item_store_ts_on),
+ getString(R.string.dev_item_store_ts_off),
+ getString(R.string.dev_item_store_ts_description)) {
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ setChecked(TunerPreferences.getStoreTsStream(getContext()));
+ }
+
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ TunerPreferences.setStoreTsStream(getContext(), isChecked());
+ }
+ });
return items;
}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 30e885a5..8df56cd2 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -235,6 +235,9 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
void onSideFragmentViewDestroyed();
}
+ /**
+ * Preloads the view holders.
+ */
public static void preloadRecycledViews(Context context) {
if (sRecycledViewPool != null) {
return;
@@ -252,6 +255,13 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
}
}
+ /**
+ * Releases the pre-loaded view holders.
+ */
+ public static void releasePreloadedRecycledViews() {
+ sRecycledViewPool = null;
+ }
+
private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {
private final LayoutInflater mLayoutInflater;
private List<Item> mItems;
diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
index faccbc66..553cd9d7 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
@@ -22,6 +22,7 @@ import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
+import android.os.Handler;
import android.view.View;
import com.android.tv.R;
@@ -42,6 +43,7 @@ public class SideFragmentManager {
private final Animator mShowAnimator;
private final Animator mHideAnimator;
+ private final Handler mHandler = new Handler();
private final Runnable mHideAllRunnable = new Runnable() {
@Override
public void run() {
@@ -154,6 +156,7 @@ public class SideFragmentManager {
}
private void hideAllInternal() {
+ mHandler.removeCallbacksAndMessages(null);
if (mFragmentCount == 0) {
return;
}
@@ -192,8 +195,8 @@ public class SideFragmentManager {
* stack. If you want to empty the back stack, call {@link #hideAll}.
*/
public void hideSidePanel(boolean withAnimation) {
+ mHandler.removeCallbacks(mHideAllRunnable);
if (withAnimation) {
- mPanel.removeCallbacks(mHideAllRunnable);
Animator hideAnimator =
AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
hideAnimator.setTarget(mPanel);
@@ -213,9 +216,12 @@ public class SideFragmentManager {
return mPanel.getVisibility() == View.VISIBLE;
}
+ /**
+ * Resets the timer for hiding side fragment.
+ */
public void scheduleHideAll() {
- mPanel.removeCallbacks(mHideAllRunnable);
- mPanel.postDelayed(mHideAllRunnable, mShowDurationMillis);
+ mHandler.removeCallbacks(mHideAllRunnable);
+ mHandler.postDelayed(mHideAllRunnable, mShowDurationMillis);
}
/**
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 8bcdb294..78243642 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -29,9 +29,9 @@ import android.util.Log;
import android.util.Range;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
+import com.android.tv.dvr.RecordedProgram;
import java.util.ArrayList;
import java.util.List;
@@ -162,7 +162,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
@Override
public String toString() {
- return this.getClass().getSimpleName() + "(" + mUri + ")";
+ return this.getClass().getName() + "(" + mUri + ")";
}
}
diff --git a/src/com/android/tv/tuner/TunerFlags.java b/src/com/android/tv/util/CompositeComparator.java
index 9370b092..47cf50fe 100644
--- a/src/com/android/tv/tuner/TunerFlags.java
+++ b/src/com/android/tv/util/CompositeComparator.java
@@ -14,11 +14,29 @@
* limitations under the License.
*/
-package com.android.tv.tuner;
+package com.android.tv.util;
+
+import java.util.Comparator;
/**
- * Defines flags which are useful for experimenting new feature on tuner implementation.
+ * A comparator which runs multiple comparators sequentially.
*/
-public final class TunerFlags {
- public static final boolean USE_EXTRACTOR_IN_EXOPLAYER = false;
+public class CompositeComparator<T> implements Comparator<T> {
+ private final Comparator<T>[] mComparators;
+
+ @SafeVarargs
+ public CompositeComparator(Comparator<T>... comparators) {
+ mComparators = comparators;
+ }
+
+ @Override
+ public int compare(T lhs, T rhs) {
+ for (Comparator<T> comparator : mComparators) {
+ int result = comparator.compare(lhs, rhs);
+ if (result != 0) {
+ return result;
+ }
+ }
+ return 0;
+ }
}
diff --git a/src/com/android/tv/util/DvrTunerStorageUtils.java b/src/com/android/tv/util/DvrTunerStorageUtils.java
deleted file mode 100644
index 534a95ef..00000000
--- a/src/com/android/tv/util/DvrTunerStorageUtils.java
+++ /dev/null
@@ -1,113 +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.util;
-
-import android.os.Environment;
-import android.content.Context;
-import android.os.StatFs;
-import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
-
-import com.android.tv.common.feature.CommonFeatures;
-
-import java.io.File;
-
-/**
- * A utility class for storage usage of DVR recording.
- */
-public class DvrTunerStorageUtils {
- // STOPSHIP: turn off ALLOW_REMOVABLE_STORAGE. b/30768857
- private static final boolean ALLOW_REMOVABLE_STORAGE = true;
-
- private static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L;
- private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES = 10 * 1024 * 1024 * 1024L;
- private static final String RECORDING_DATA_SUB_PATH = "/recording/";
- // Since {@link StorageVolume#getUuid} will return null for internal storage and {@code null}
- // should be used for missing storage status, we need the internal storage specifier.
- private static final String INTERNAL_STORAGE_UUID = "internal_storage_uuid";
-
- /**
- * Returns the path to DVR recording data directory.
- * @param context {@link Context}
- * @param recordingId unique {@link String} specifier for each recording
- * @return {@link File}
- */
- public static File getRecordingDataDirectory(Context context, String recordingId) {
- File recordingDataRootDir = getRootDirectory(context);
- if (recordingDataRootDir == null) {
- return null;
- }
- return new File(recordingDataRootDir + RECORDING_DATA_SUB_PATH + recordingId);
- }
-
- /**
- * Returns the unique identifier for the storage which will be used to store recordings.
- * @param context {@link Context}
- * @return {@link String} of the unique identifier when storage exists, {@code null} otherwise
- */
- public static String getRecordingStorageUuid(Context context) {
- File recordingDataRootDir = getRootDirectory(context);
- StorageManager manager = (StorageManager) context.getSystemService(context.STORAGE_SERVICE);
- StorageVolume volume = manager.getStorageVolume(recordingDataRootDir);
- if (volume == null) {
- return null;
- }
- if (!Environment.MEDIA_MOUNTED.equals(volume.getState())) {
- return null;
- }
- String uuid = volume.getUuid();
- return uuid == null ? INTERNAL_STORAGE_UUID : uuid;
- }
-
- /**
- * Returns whether the storage has sufficient storage.
- * @param context {@link Context}
- * @return {@code true} when there is sufficient storage, {@code false} otherwise
- */
- public static boolean isStorageSufficient(Context context) {
- File recordingDataRootDir = getRootDirectory(context);
- if (recordingDataRootDir == null || !recordingDataRootDir.isDirectory()) {
- return false;
- }
- if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(context)) {
- return true;
- }
- StatFs statFs = new StatFs(recordingDataRootDir.toString());
- return statFs.getTotalBytes() >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES
- && statFs.getAvailableBytes() >= MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES;
- }
-
- private static File getRootDirectory(Context context) {
- if (!ALLOW_REMOVABLE_STORAGE) {
- return context.getExternalFilesDir(null);
- }
- File[] dirs = context.getExternalFilesDirs(null);
- if (dirs == null) {
- return null;
- }
- for (File dir : dirs) {
- if (dir == null) {
- continue;
- }
- StatFs statFs = new StatFs(dir.toString());
- if (statFs.getTotalBytes() >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES
- && statFs.getAvailableBytes() >= MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) {
- return dir;
- }
- }
- return dirs[0];
- }
-}
diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java
index 406a9d88..8e3b59e9 100644
--- a/src/com/android/tv/util/LocationUtils.java
+++ b/src/com/android/tv/util/LocationUtils.java
@@ -25,6 +25,7 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
+
import java.io.IOException;
import java.util.List;
import java.util.Locale;
@@ -36,36 +37,6 @@ public class LocationUtils {
private static final String TAG = "LocationUtils";
private static final boolean DEBUG = false;
- private static final LocationListener LOCATION_LISTENER = new LocationListener() {
- @Override
- public void onLocationChanged(Location location) {
- Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault());
- try {
- List<Address> addresses = geocoder.getFromLocation(location.getLatitude(),
- location.getLongitude(), 1);
- if (addresses != null) {
- sAddress = addresses.get(0);
- if (DEBUG) Log.d(TAG, "returned address: " + sAddress);
- } else {
- if (DEBUG) Log.d(TAG, "No address returned");
- }
- sError = null;
- } catch (IOException e) {
- Log.w(TAG, "Error in retrieving address", e);
- sError = e;
- }
- }
-
- @Override
- public void onStatusChanged(String provider, int status, Bundle extras) { }
-
- @Override
- public void onProviderEnabled(String provider) { }
-
- @Override
- public void onProviderDisabled(String provider) { }
- };
-
private static Context sApplicationContext;
private static Address sAddress;
private static IOException sError;
@@ -83,19 +54,67 @@ public class LocationUtils {
}
if (sApplicationContext == null) {
sApplicationContext = context.getApplicationContext();
- LocationManager mLocationManager = (LocationManager) context.getSystemService(
- Context.LOCATION_SERVICE);
- try {
- mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 10,
- LOCATION_LISTENER, null);
- } catch (SecurityException e) {
- // Enables requesting the location updates again.
- sApplicationContext = null;
- throw e;
- }
}
+ LocationUtilsHelper.startLocationUpdates();
return null;
}
+ private static void updateAddress(Location location) {
+ if (DEBUG) Log.d(TAG, "Updating address with " + location);
+ if (location == null) {
+ return;
+ }
+ Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault());
+ try {
+ List<Address> addresses = geocoder.getFromLocation(
+ location.getLatitude(), location.getLongitude(), 1);
+ if (addresses != null) {
+ sAddress = addresses.get(0);
+ if (DEBUG) Log.d(TAG, "Got " + sAddress);
+ } else {
+ if (DEBUG) Log.d(TAG, "No address returned");
+ }
+ sError = null;
+ } catch (IOException e) {
+ Log.w(TAG, "Error in updating address", e);
+ sError = e;
+ }
+ }
+
private LocationUtils() { }
+
+ private static class LocationUtilsHelper {
+ private static final LocationListener LOCATION_LISTENER = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ updateAddress(location);
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) { }
+
+ @Override
+ public void onProviderEnabled(String provider) { }
+
+ @Override
+ public void onProviderDisabled(String provider) { }
+ };
+
+ private static LocationManager sLocationManager;
+
+ public static void startLocationUpdates() {
+ if (sLocationManager == null) {
+ sLocationManager = (LocationManager) sApplicationContext.getSystemService(
+ Context.LOCATION_SERVICE);
+ try {
+ sLocationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null);
+ } catch (SecurityException e) {
+ // Enables requesting the location updates again.
+ sLocationManager = null;
+ throw e;
+ }
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java
index a443ede7..453885a4 100644
--- a/src/com/android/tv/util/PermissionUtils.java
+++ b/src/com/android/tv/util/PermissionUtils.java
@@ -7,6 +7,11 @@ import android.content.pm.PackageManager;
* Util class to handle permissions.
*/
public class PermissionUtils {
+ /**
+ * Permission to read the TV listings.
+ */
+ public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
private static Boolean sHasAccessAllEpgPermission;
private static Boolean sHasAccessWatchedHistoryPermission;
private static Boolean sHasModifyParentalControlsPermission;
@@ -39,7 +44,7 @@ public class PermissionUtils {
}
public static boolean hasReadTvListings(Context context) {
- return context.checkSelfPermission("android.permission.READ_TV_LISTINGS")
+ return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
== PackageManager.PERMISSION_GRANTED;
}
}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
index 03bdc681..2c51d5a0 100644
--- a/src/com/android/tv/util/PipInputManager.java
+++ b/src/com/android/tv/util/PipInputManager.java
@@ -149,6 +149,7 @@ public class PipInputManager {
if (mStarted) {
return;
}
+ mStarted = true;
mInputManager.addCallback(mTvInputCallback);
mChannelTuner.addListener(mChannelTunerListener);
initializePipInputList();
@@ -161,6 +162,7 @@ public class PipInputManager {
if (!mStarted) {
return;
}
+ mStarted = false;
mInputManager.removeCallback(mTvInputCallback);
mChannelTuner.removeListener(mChannelTunerListener);
mPipInputMap.clear();
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 469170ed..4135bd4e 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -126,10 +126,7 @@ public final class RecurringRunner {
return next;
}
- /**
- * Resets the next run time.
- */
- public long resetNextRunTime() {
+ private long resetNextRunTime() {
long next = System.currentTimeMillis() + mIntervalMs;
getSharedPreferences().edit().putLong(mName, next).apply();
return next;
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 1213d317..8223a81c 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -20,6 +20,8 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
@@ -35,6 +37,8 @@ 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.epg.EpgFetcher;
+import com.android.tv.experiments.Experiments;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import java.util.Collections;
@@ -267,7 +271,8 @@ public class SetupUtils {
// Find all already-verified packages.
Set<String> setUpPackages = new HashSet<>();
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
- for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) {
+ for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS,
+ Collections.<String>emptySet())) {
if (!TextUtils.isEmpty(input)) {
ComponentName componentName = ComponentName.unflattenFromString(input);
if (componentName != null) {
@@ -330,14 +335,28 @@ public class SetupUtils {
removedInputList.remove(mTunerInputId);
if (!removedInputList.isEmpty()) {
+ boolean inputPackageDeleted = false;
for (String input : removedInputList) {
- mRecognizedInputs.remove(input);
- mSetUpInputs.remove(input);
- mKnownInputs.remove(input);
+ try {
+ // Just after booting, input list from TvInputManager are not reliable.
+ // So we need to double-check package existence. b/29034900
+ mTvApplication.getPackageManager().getPackageInfo(
+ ComponentName.unflattenFromString(input)
+ .getPackageName(), PackageManager.GET_ACTIVITIES);
+ Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted");
+ } catch (NameNotFoundException e) {
+ Log.i(TAG, "TV input (" + input + ") and its package are removed");
+ mRecognizedInputs.remove(input);
+ mSetUpInputs.remove(input);
+ mKnownInputs.remove(input);
+ inputPackageDeleted = true;
+ }
+ }
+ if (inputPackageDeleted) {
+ mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
+ .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
+ .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
}
- mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
- .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
- .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
}
}
@@ -345,7 +364,7 @@ public class SetupUtils {
* Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
* for {@code inputId}.
*/
- public void onSetupDone(String inputId) {
+ private void onSetupDone(String inputId) {
SoftPreconditions.checkState(inputId != null);
if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
if (!mRecognizedInputs.contains(inputId)) {
@@ -363,5 +382,13 @@ public class SetupUtils {
mSetUpInputs.add(inputId);
mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
}
+ // Start fetching program guide data for internal tuners.
+ Context context = mTvApplication.getApplicationContext();
+ if (Utils.isInternalTvInput(context, inputId)) {
+ if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) {
+ EpgFetcher.getInstance(context).startImmediately();
+ }
+ }
}
}
diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java
index 235161b6..e737f233 100644
--- a/src/com/android/tv/util/SystemProperties.java
+++ b/src/com/android/tv/util/SystemProperties.java
@@ -36,12 +36,6 @@ public final class SystemProperties {
"tv_allow_strict_mode", true);
/**
- * Allow Strict death penalty for eng builds.
- */
- public static final BooleanSystemProperty ALLOW_DEATH_PENALTY = new BooleanSystemProperty(
- "tv_allow_death_penalty", true);
-
- /**
* When true {@link android.view.KeyEvent}s are logged. Defaults to false.
*/
public static final BooleanSystemProperty LOG_KEYEVENT = new BooleanSystemProperty(
diff --git a/src/com/android/tv/util/ToastUtils.java b/src/com/android/tv/util/ToastUtils.java
new file mode 100644
index 00000000..34346b2a
--- /dev/null
+++ b/src/com/android/tv/util/ToastUtils.java
@@ -0,0 +1,43 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.widget.Toast;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A utility class for the toast message.
+ */
+public class ToastUtils {
+ private static WeakReference<Toast> sToast;
+
+ /**
+ * Shows the toast message after canceling the previous one.
+ */
+ @MainThread
+ public static void show(Context context, CharSequence text, int duration) {
+ if (sToast != null && sToast.get() != null) {
+ sToast.get().cancel();
+ }
+ Toast toast = Toast.makeText(context, text, duration);
+ toast.show();
+ sToast = new WeakReference<>(toast);
+ }
+}
diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java
index 4a65cbbe..97ff59d6 100644
--- a/src/com/android/tv/util/TvSettings.java
+++ b/src/com/android/tv/util/TvSettings.java
@@ -34,9 +34,6 @@ import java.util.Set;
public final class TvSettings {
private TvSettings() {}
- public static final String PREFS_FILE = "settings";
- public static final String PREF_TV_WATCH_LOGGING_ENABLED = "tv_watch_logging_enabled";
- public static final String PREF_CLOSED_CAPTION_ENABLED = "is_cc_enabled"; // boolean value
public static final String PREF_DISPLAY_MODE = "display_mode"; // int value
public static final String PREF_PIP_LAYOUT = "pip_layout"; // int value
public static final String PREF_PIP_SIZE = "pip_size"; // int value
@@ -49,7 +46,6 @@ public final class TvSettings {
public @interface PipSound {}
public static final int PIP_SOUND_MAIN = 0;
public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1;
- public static final int PIP_SOUND_LAST = PIP_SOUND_PIP_WINDOW;
// PIP layouts
@Retention(RetentionPolicy.SOURCE)
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 9f90c69f..99d34431 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -37,6 +37,7 @@ import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.view.View;
@@ -77,7 +78,6 @@ public class Utils {
public static final String EXTRA_KEY_ACTION = "action";
public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input";
public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher";
- public static final String EXTRA_KEY_RECORDING_URI = "recording_uri";
public static final String EXTRA_KEY_RECORDED_PROGRAM_ID = "recorded_program_id";
public static final String EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME = "recorded_program_seek_time";
public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED =
@@ -119,7 +119,7 @@ public class Utils {
// Hardcoded list for known bundled inputs not written by OEM/SOCs.
// Bundled (system) inputs not in the list will get the high priority
// so they and their channels come first in the UI.
- private static final Set<String> BUNDLED_PACKAGE_SET = new HashSet<>();
+ private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>();
static {
BUNDLED_PACKAGE_SET.add("com.android.tv");
@@ -803,6 +803,18 @@ public class Utils {
}
/**
+ * Checks whether a given input is a bundled input.
+ */
+ public static boolean isBundledInput(String inputId) {
+ for (String prefix : BUNDLED_PACKAGE_SET) {
+ if (inputId.startsWith(prefix + "/")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Returns the canonical genre ID's from the {@code genres}.
*/
public static int[] getCanonicalGenreIds(String genres) {