aboutsummaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/tv/Features.java64
-rw-r--r--src/com/android/tv/InputSessionManager.java28
-rw-r--r--src/com/android/tv/MainActivity.java764
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java90
-rw-r--r--src/com/android/tv/TimeShiftManager.java9
-rw-r--r--src/com/android/tv/TvApplication.java19
-rw-r--r--src/com/android/tv/TvOptionsManager.java133
-rw-r--r--src/com/android/tv/data/Channel.java97
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java42
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java307
-rw-r--r--src/com/android/tv/data/ChannelNumber.java71
-rw-r--r--src/com/android/tv/data/InternalDataUtils.java2
-rw-r--r--src/com/android/tv/data/StreamInfo.java4
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java327
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java29
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java17
-rw-r--r--src/com/android/tv/dialog/DvrHistoryDialogFragment.java129
-rw-r--r--src/com/android/tv/dialog/FullscreenDialogFragment.java2
-rw-r--r--src/com/android/tv/dialog/HalfSizedDialogFragment.java (renamed from src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java)24
-rw-r--r--src/com/android/tv/dialog/SafeDismissDialogFragment.java26
-rw-r--r--src/com/android/tv/dialog/WebDialogFragment.java17
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java41
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java12
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java143
-rw-r--r--src/com/android/tv/dvr/DvrManager.java81
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java139
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java35
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java1
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java6
-rw-r--r--src/com/android/tv/dvr/data/IdGenerator.java (renamed from src/com/android/tv/dvr/IdGenerator.java)2
-rw-r--r--src/com/android/tv/dvr/data/RecordedProgram.java (renamed from src/com/android/tv/dvr/RecordedProgram.java)2
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java (renamed from src/com/android/tv/dvr/ScheduledRecording.java)17
-rw-r--r--src/com/android/tv/dvr/data/SeasonEpisodeNumber.java72
-rw-r--r--src/com/android/tv/dvr/data/SeriesInfo.java (renamed from src/com/android/tv/dvr/SeriesInfo.java)2
-rw-r--r--src/com/android/tv/dvr/data/SeriesRecording.java (renamed from src/com/android/tv/dvr/SeriesRecording.java)3
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java4
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java4
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java (renamed from src/com/android/tv/dvr/DvrDbSync.java)34
-rw-r--r--src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java (renamed from src/com/android/tv/dvr/EpisodicProgramLoadTask.java)73
-rw-r--r--src/com/android/tv/dvr/recorder/ConflictChecker.java (renamed from src/com/android/tv/dvr/ConflictChecker.java)5
-rw-r--r--src/com/android/tv/dvr/recorder/DvrRecordingService.java (renamed from src/com/android/tv/dvr/DvrRecordingService.java)36
-rw-r--r--src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java (renamed from src/com/android/tv/dvr/DvrStartRecordingReceiver.java)2
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java (renamed from src/com/android/tv/dvr/InputTaskScheduler.java)6
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java (renamed from src/com/android/tv/dvr/RecordingTask.java)23
-rw-r--r--src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java (renamed from src/com/android/tv/dvr/ScheduledProgramReaper.java)5
-rw-r--r--src/com/android/tv/dvr/recorder/Scheduler.java (renamed from src/com/android/tv/dvr/Scheduler.java)6
-rw-r--r--src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java (renamed from src/com/android/tv/dvr/SeriesRecordingScheduler.java)81
-rw-r--r--src/com/android/tv/dvr/ui/BigArguments.java54
-rw-r--r--src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java79
-rw-r--r--src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java59
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java7
-rw-r--r--src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java50
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java12
-rw-r--r--src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java84
-rw-r--r--src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java50
-rw-r--r--src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java (renamed from src/com/android/tv/dvr/ui/PrioritySettingsFragment.java)7
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java17
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java5
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesDeletionFragment.java)11
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java45
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java20
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesSettingsFragment.java)209
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java11
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java (renamed from src/com/android/tv/dvr/DvrUiHelper.java)311
-rw-r--r--src/com/android/tv/dvr/ui/FadeBackground.java70
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java90
-rw-r--r--src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java (renamed from src/com/android/tv/dvr/ui/ActionPresenterSelector.java)10
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java120
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java (renamed from src/com/android/tv/dvr/ui/DetailsContent.java)4
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java (renamed from src/com/android/tv/dvr/ui/DetailsContentPresenter.java)37
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java (renamed from src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java)4
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java (renamed from src/com/android/tv/dvr/ui/DvrActivity.java)6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java (renamed from src/com/android/tv/dvr/ui/DvrBrowseFragment.java)159
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java (renamed from src/com/android/tv/dvr/ui/DvrDetailsActivity.java)2
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/DvrDetailsFragment.java)6
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java (renamed from src/com/android/tv/dvr/ui/DvrItemPresenter.java)13
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java34
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java (renamed from src/com/android/tv/dvr/ui/FullScheduleCardHolder.java)2
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java (renamed from src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java)66
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java)6
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java (renamed from src/com/android/tv/dvr/ui/RecordedProgramPresenter.java)31
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java (renamed from src/com/android/tv/dvr/ui/RecordingCardView.java)113
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/RecordingDetailsFragment.java)4
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java)4
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java (renamed from src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java)21
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java)24
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java (renamed from src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java)11
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java (renamed from src/com/android/tv/dvr/ui/DvrSchedulesActivity.java)76
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java72
-rw-r--r--src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java6
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java4
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java4
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java50
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java20
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java26
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java24
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java17
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java (renamed from src/com/android/tv/dvr/DvrPlaybackActivity.java)4
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java (renamed from src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java)31
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java (renamed from src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java)106
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java (renamed from src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java)104
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java (renamed from src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java)181
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java154
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlayer.java (renamed from src/com/android/tv/dvr/DvrPlayer.java)220
-rw-r--r--src/com/android/tv/experiments/ExperimentFlag.java32
-rw-r--r--src/com/android/tv/experiments/Experiments.java9
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java2
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java21
-rw-r--r--src/com/android/tv/guide/ProgramManager.java20
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java68
-rw-r--r--src/com/android/tv/guide/TimeListAdapter.java27
-rw-r--r--src/com/android/tv/menu/ActionCardView.java6
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java198
-rw-r--r--src/com/android/tv/menu/BaseCardView.java50
-rw-r--r--src/com/android/tv/menu/ChannelCardView.java8
-rw-r--r--src/com/android/tv/menu/ChannelsRow.java10
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java4
-rw-r--r--src/com/android/tv/menu/ItemListRowView.java14
-rw-r--r--src/com/android/tv/menu/Menu.java49
-rw-r--r--src/com/android/tv/menu/MenuAction.java69
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java20
-rw-r--r--src/com/android/tv/menu/MenuRowFactory.java24
-rw-r--r--src/com/android/tv/menu/MenuUpdater.java41
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java50
-rw-r--r--src/com/android/tv/menu/PartnerOptionsRowAdapter.java3
-rw-r--r--src/com/android/tv/menu/PipOptionsRowAdapter.java137
-rw-r--r--src/com/android/tv/menu/PlayControlsButton.java24
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java194
-rw-r--r--src/com/android/tv/menu/PlaybackProgressBar.java168
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java128
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java2
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java2
-rw-r--r--src/com/android/tv/receiver/GlobalKeyReceiver.java40
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java6
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java1
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java4
-rw-r--r--src/com/android/tv/search/SearchInterface.java2
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java14
-rw-r--r--src/com/android/tv/tuner/ChannelScanFileParser.java2
-rw-r--r--src/com/android/tv/tuner/TunerHal.java43
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java195
-rw-r--r--src/com/android/tv/tuner/TunerPreferences.java97
-rw-r--r--src/com/android/tv/tuner/UsbTunerHal.java6
-rw-r--r--src/com/android/tv/tuner/cc/CaptionLayout.java2
-rw-r--r--src/com/android/tv/tuner/cc/CaptionTrackRenderer.java2
-rw-r--r--src/com/android/tv/tuner/cc/Cea708Parser.java5
-rw-r--r--src/com/android/tv/tuner/data/PsiData.java4
-rw-r--r--src/com/android/tv/tuner/data/PsipData.java6
-rw-r--r--src/com/android/tv/tuner/data/TunerChannel.java130
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java378
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java14
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java62
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java13
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java)114
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java)21
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java4
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java20
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java280
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java209
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java115
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java85
-rw-r--r--src/com/android/tv/tuner/setup/ConnectionTypeFragment.java18
-rw-r--r--src/com/android/tv/tuner/setup/PostalCodeFragment.java184
-rw-r--r--src/com/android/tv/tuner/setup/ScanFragment.java58
-rw-r--r--src/com/android/tv/tuner/setup/ScanResultFragment.java17
-rw-r--r--src/com/android/tv/tuner/setup/TunerSetupActivity.java238
-rw-r--r--src/com/android/tv/tuner/setup/WelcomeFragment.java38
-rw-r--r--src/com/android/tv/tuner/source/FileTsStreamer.java2
-rw-r--r--src/com/android/tv/tuner/source/TsDataSourceManager.java12
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamer.java58
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamerManager.java26
-rw-r--r--src/com/android/tv/tuner/ts/SectionParser.java20
-rw-r--r--src/com/android/tv/tuner/ts/TsParser.java18
-rw-r--r--src/com/android/tv/tuner/tvinput/ChannelDataManager.java54
-rw-r--r--src/com/android/tv/tuner/tvinput/EventDetector.java52
-rw-r--r--src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java4
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java116
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSession.java17
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSessionWorker.java300
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerTvInputService.java30
-rw-r--r--src/com/android/tv/tuner/util/PostalCodeUtils.java89
-rw-r--r--src/com/android/tv/tuner/util/SystemPropertiesProxy.java16
-rw-r--r--src/com/android/tv/tuner/util/TunerInputInfoUtils.java46
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java10
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java137
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java2
-rw-r--r--src/com/android/tv/ui/SelectInputView.java73
-rw-r--r--src/com/android/tv/ui/TunableTvView.java336
-rw-r--r--src/com/android/tv/ui/TuningBlockView.java113
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java30
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java265
-rw-r--r--src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java9
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java23
-rw-r--r--src/com/android/tv/ui/sidepanel/Item.java12
-rw-r--r--src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java170
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java20
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java47
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragmentManager.java2
-rw-r--r--src/com/android/tv/ui/sidepanel/SimpleActionItem.java (renamed from src/com/android/tv/ui/sidepanel/SimpleItem.java)6
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java2
-rw-r--r--src/com/android/tv/util/Debug.java60
-rw-r--r--src/com/android/tv/util/DurationTimer.java (renamed from src/com/android/tv/analytics/DurationTimer.java)24
-rw-r--r--src/com/android/tv/util/LocationUtils.java24
-rw-r--r--src/com/android/tv/util/Partner.java181
-rw-r--r--src/com/android/tv/util/PipInputManager.java432
-rw-r--r--src/com/android/tv/util/RecurringRunner.java9
-rw-r--r--src/com/android/tv/util/SearchManagerHelper.java61
-rw-r--r--src/com/android/tv/util/SetupUtils.java20
-rw-r--r--src/com/android/tv/util/StringUtils.java (renamed from src/com/android/tv/tuner/util/StringUtils.java)2
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java332
-rw-r--r--src/com/android/tv/util/TvSettings.java148
-rw-r--r--src/com/android/tv/util/TvTrackInfoUtils.java37
-rw-r--r--src/com/android/tv/util/Utils.java65
-rw-r--r--src/com/android/tv/util/ViewCache.java70
225 files changed, 8347 insertions, 5202 deletions
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
index 7e8e3689..d8b7ae26 100644
--- a/src/com/android/tv/Features.java
+++ b/src/com/android/tv/Features.java
@@ -27,11 +27,18 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.VisibleForTesting;
import android.support.v4.os.BuildCompat;
+import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.common.feature.Feature;
import com.android.tv.common.feature.GServiceFeature;
import com.android.tv.common.feature.PropertyFeature;
+import com.android.tv.config.RemoteConfig;
+import com.android.tv.util.LocationUtils;
import com.android.tv.util.PermissionUtils;
+import com.android.tv.util.Utils;
+
+import java.util.Locale;
/**
* List of {@link Feature} for the Live TV App.
@@ -39,6 +46,9 @@ import com.android.tv.util.PermissionUtils;
* <p>Remove the {@code Feature} once it is launched.
*/
public final class Features {
+ private static final String TAG = "Features";
+ private static final boolean DEBUG = false;
+
/**
* UI for opting in to analytics.
*
@@ -61,6 +71,11 @@ public final class Features {
@Override
public boolean isEnabled(Context context) {
+ if (Utils.isDeveloper()) {
+ // we enable tuner for developers to test tuner in any platform.
+ return true;
+ }
+
// This is special handling just for USB Tuner.
// It does not require any N API's but relies on a improvements in N for AC3 support
// After release, change class to this to just be {@link BuildCompat#isAtLeastN()}.
@@ -69,6 +84,25 @@ public final class Features {
};
+ /**
+ * Use network tuner if it is available and there is no other tuner types.
+ */
+ public static final Feature NETWORK_TUNER =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
+ if (!TUNER.isEnabled(context)) {
+ return false;
+ }
+ if (Utils.isDeveloper()) {
+ // Network tuner will be enabled for developers.
+ return true;
+ }
+ return Locale.US.getCountry().equalsIgnoreCase(
+ LocationUtils.getCurrentCountry(context));
+ }
+ };
+
private static final String GSERVICE_KEY_UNHIDE = "live_channels_unhide";
/**
* A flag which indicates that LC app is unhidden even when there is no input.
@@ -96,6 +130,36 @@ public final class Features {
};
/**
+ * Use AC3 software decode.
+ */
+ public static final Feature AC3_SOFTWARE_DECODE =
+ new Feature() {
+ private final String[] SUPPORTED_COUNTRIES = {
+ };
+
+ private Boolean mEnabled;
+
+ @Override
+ public boolean isEnabled(Context context) {
+ if (mEnabled == null) {
+ if (mEnabled == null) {
+ // We will not cache the result of fallback solution.
+ String country = LocationUtils.getCurrentCountry(context);
+ for (int i = 0; i < SUPPORTED_COUNTRIES.length; ++i) {
+ if (SUPPORTED_COUNTRIES[i].equalsIgnoreCase(country)) {
+ return true;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "AC3 flag false after country check");
+ return false;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "AC3 flag " + mEnabled);
+ return mEnabled;
+ }
+ };
+
+ /**
* Enable a conflict dialog between currently watched channel and upcoming recording.
*/
public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF;
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index e4b0f456..faf76555 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -37,7 +37,6 @@ import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
@@ -73,6 +72,8 @@ public class InputSessionManager {
Collections.synchronizedSet(new ArraySet<>());
private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners =
new ArraySet<>();
+ private final Set<OnRecordingSessionChangeListener> mOnRecordingSessionChangeListeners =
+ new ArraySet<>();
public InputSessionManager(Context context) {
mContext = context.getApplicationContext();
@@ -113,6 +114,9 @@ public class InputSessionManager {
RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
mRecordingSessions.add(session);
if (DEBUG) Log.d(TAG, "Recording session created: " + session);
+ for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) {
+ listener.onRecordingSessionChange(true, mRecordingSessions.size());
+ }
return session;
}
@@ -123,6 +127,9 @@ public class InputSessionManager {
mRecordingSessions.remove(session);
session.release();
if (DEBUG) Log.d(TAG, "Recording session released: " + session);
+ for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) {
+ listener.onRecordingSessionChange(false, mRecordingSessions.size());
+ }
}
/**
@@ -148,9 +155,17 @@ public class InputSessionManager {
}
}
- /**
- * Returns the current {@link TvView} channel.
- */
+ /** Adds the {@link OnRecordingSessionChangeListener}. */
+ public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
+ mOnRecordingSessionChangeListeners.add(listener);
+ }
+
+ /** Removes the {@link OnRecordingSessionChangeListener}. */
+ public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
+ mOnRecordingSessionChangeListeners.remove(listener);
+ }
+
+ /** Returns the current {@link TvView} channel. */
@MainThread
public Uri getCurrentTvViewChannelUri() {
for (TvViewSession session : mTvViewSessions) {
@@ -546,4 +561,9 @@ public class InputSessionManager {
public interface OnTvViewChannelChangeListener {
void onTvViewChannelChange(@Nullable Uri channelUri);
}
+
+ /** Called when recording session is created or destroyed. */
+ public interface OnRecordingSessionChangeListener {
+ void onRecordingSessionChange(boolean create, int count);
+ }
}
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index 58850b5f..94006b72 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -29,7 +29,6 @@ import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.media.AudioManager;
import android.media.MediaMetadata;
@@ -71,7 +70,6 @@ import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.Toast;
-import com.android.tv.analytics.DurationTimer;
import com.android.tv.analytics.SendChannelStatusRunnable;
import com.android.tv.analytics.SendConfigInfoRunnable;
import com.android.tv.analytics.Tracker;
@@ -91,14 +89,14 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.data.StreamInfo;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
-import com.android.tv.dvr.ConflictChecker;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.recorder.ConflictChecker;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.experiments.Experiments;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
@@ -107,9 +105,11 @@ import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.receiver.AudioCapabilitiesReceiver;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.search.ProgramGuideSearchFragment;
+import com.android.tv.tuner.TunerInputController;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.setup.TunerSetupActivity;
import com.android.tv.tuner.tvinput.TunerTvInputService;
+import com.android.tv.tuner.util.PostalCodeUtils;
import com.android.tv.ui.AppLayerTvView;
import com.android.tv.ui.ChannelBannerView;
import com.android.tv.ui.InputBannerView;
@@ -119,6 +119,7 @@ import com.android.tv.ui.SelectInputView.OnInputSelectedCallback;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.BlockScreenType;
import com.android.tv.ui.TunableTvView.OnTuneListener;
+import com.android.tv.ui.TuningBlockView;
import com.android.tv.ui.TvOverlayManager;
import com.android.tv.ui.TvViewUiManager;
import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
@@ -130,21 +131,20 @@ import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.util.AccountHelper;
import com.android.tv.util.CaptionSettings;
+import com.android.tv.util.Debug;
+import com.android.tv.util.DurationTimer;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.PermissionUtils;
-import com.android.tv.util.PipInputManager;
-import com.android.tv.util.PipInputManager.PipInput;
import com.android.tv.util.RecurringRunner;
-import com.android.tv.util.SearchManagerHelper;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvSettings;
-import com.android.tv.util.TvSettings.PipSound;
import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
+import com.android.tv.util.ViewCache;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -181,8 +181,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private static final float FRAME_RATE_FOR_FILM = 23.976f;
private static final float FRAME_RATE_EPSILON = 0.1f;
- private static final float MEDIA_SESSION_STOPPED_SPEED = 0.0f;
- private static final float MEDIA_SESSION_PLAYING_SPEED = 1.0f;
+ private static PlaybackState MEDIA_SESSION_STATE_PLAYING = new PlaybackState.Builder()
+ .setState(PlaybackState.STATE_PLAYING, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 1.0f)
+ .build();
+ private static PlaybackState MEDIA_SESSION_STATE_STOPPED = new PlaybackState.Builder()
+ .setState(PlaybackState.STATE_STOPPED, PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0.0f)
+ .build();
private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
@@ -227,12 +231,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private static final int MSG_CHANNEL_DOWN_PRESSED = 1000;
private static final int MSG_CHANNEL_UP_PRESSED = 1001;
- private static final int MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE = 1002;
@Retention(RetentionPolicy.SOURCE)
@IntDef({UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW, UPDATE_CHANNEL_BANNER_REASON_TUNE,
UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO,
- UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK})
+ UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK,
+ UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO})
private @interface ChannelBannerUpdateReason {}
/**
* Updates channel banner because the channel banner is forced to show.
@@ -254,6 +258,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* Updates channel banner because the current watched channel is locked or unlocked.
*/
private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
+ /**
+ * Updates channel banner because of stream info updating.
+ */
+ private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6;
private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000;
@@ -261,12 +269,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// Delay 1 second in order not to interrupt the first tune.
private static final long LAZY_INITIALIZATION_DELAY = TimeUnit.SECONDS.toMillis(1);
+ private static final int UNDEFINED_TRACK_INDEX = -1;
+
private AccessibilityManager mAccessibilityManager;
private ChannelDataManager mChannelDataManager;
private ProgramDataManager mProgramDataManager;
private TvInputManagerHelper mTvInputManagerHelper;
private ChannelTuner mChannelTuner;
- private PipInputManager mPipInputManager;
private final TvOptionsManager mTvOptionsManager = new TvOptionsManager(this);
private TvViewUiManager mTvViewUiManager;
private TimeShiftManager mTimeShiftManager;
@@ -278,7 +287,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private View mContentView;
private TunableTvView mTvView;
- private TunableTvView mPipView;
private Bundle mTuneParams;
private boolean mChannelBannerHiddenBySideFragment;
// TODO: Move the scene views into TvTransitionManager or TvOverlayManager.
@@ -303,10 +311,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private AudioManager mAudioManager;
private int mAudioFocusStatus;
private boolean mTunePending;
- private boolean mPipEnabled;
- private Channel mPipChannel;
- private boolean mPipSwap;
- @PipSound private int mPipSound = TvSettings.PIP_SOUND_MAIN; // Default
private boolean mDebugNonFullSizeScreen;
private boolean mActivityResumed;
private boolean mActivityStarted;
@@ -331,7 +335,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mIsCurrentChannelUnblockedByUser;
private boolean mWasChannelUnblockedBeforeShrunkenByUser;
private Channel mChannelBeforeShrunkenTvView;
- private Channel mPipChannelBeforeShrunkenTvView;
private boolean mIsCompletingShrunkenTvView;
// TODO: Need to consider the case that TIS explicitly request PIN code while TV view is
@@ -350,9 +353,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private RecurringRunner mSendConfigInfoRecurringRunner;
private RecurringRunner mChannelStatusRecurringRunner;
- // A caller which started this activity. (e.g. TvSearch)
- private String mSource;
-
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
@@ -372,15 +372,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!mActivityResumed && mVisibleBehind) {
// ACTION_SCREEN_ON is usually called after onResume. But, if media is played
// under launcher with requestVisibleBehind(true), onResume will not be called.
- // In this case, we need to resume TvView and PipView explicitly.
+ // In this case, we need to resume TvView explicitly.
resumeTvIfNeeded();
- resumePipIfNeeded();
}
} else if (intent.getAction().equals(
TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED)) {
if (DEBUG) Log.d(TAG, "Received parental control settings change");
- checkChannelLockNeeded(mTvView);
- checkChannelLockNeeded(mPipView);
+ checkChannelLockNeeded(mTvView, null);
applyParentalControlSettings();
}
}
@@ -407,10 +405,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
new ChannelTuner.Listener() {
@Override
public void onLoadFinished() {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "MainActivity.mChannelTunerListener.onLoadFinished");
SetupUtils.getInstance(MainActivity.this).markNewChannelsBrowsable();
if (mActivityResumed) {
resumeTvIfNeeded();
- resumePipIfNeeded();
}
mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList());
}
@@ -430,13 +429,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
};
- private final Runnable mRestoreMainViewRunnable =
- new Runnable() {
- @Override
- public void run() {
- restoreMainTvView();
- }
- };
+ private final Runnable mRestoreMainViewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ restoreMainTvView();
+ }
+ };
private ProgramGuideSearchFragment mSearchFragment;
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@@ -456,11 +454,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings()
.isParentalControlsEnabled();
mTvView.onParentalControlChanged(parentalControlEnabled);
- mPipView.onParentalControlChanged(parentalControlEnabled);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
+ // Restarts the global duration timer to avoid the case that TvApplication starts much
+ // earlier than MainActivity.
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).start();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate restarts timer");
if (DEBUG) Log.d(TAG,"onCreate()");
TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
@@ -478,32 +479,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
- // Check this permission for the EPG fetch.
- // TODO: check {@link shouldShowRequestPermissionRationale}.
- // 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);
- }
-
- DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
- Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
- Point size = new Point();
- display.getSize(size);
- int screenWidth = size.x;
- int screenHeight = size.y;
- mDefaultRefreshRate = display.getRefreshRate();
-
setContentView(R.layout.activity_tv);
- mContentView = findViewById(android.R.id.content);
+ TvApplication tvApplication = (TvApplication) getApplication();
+ mProgramDataManager = tvApplication.getProgramDataManager();
+ mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
mTvView = (TunableTvView) findViewById(R.id.main_tunable_tv_view);
- int shrunkenTvViewHeight = getResources().getDimensionPixelSize(
- R.dimen.shrunken_tvview_height);
- mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view), false, screenHeight,
- shrunkenTvViewHeight);
+ mTvView.initialize((AppLayerTvView) findViewById(R.id.main_tv_view),
+ (TuningBlockView) findViewById(R.id.tuning_block), mProgramDataManager,
+ mTvInputManagerHelper);
mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
@@ -526,7 +509,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return false;
}
});
-
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
if (!isPassthroughInput && inputId != null
@@ -534,27 +516,34 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId));
}
- TvApplication tvApplication = (TvApplication) getApplication();
+ // Check this permission for the EPG fetch.
+ // TODO: check {@link shouldShowRequestPermissionRationale}.
+ // 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)
+ && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this))
+ && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
+ PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+ }
+
tvApplication.getMainActivityWrapper().onMainActivityCreated(this);
if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
Toast.makeText(this, "Using Strict Mode for eng builds", Toast.LENGTH_SHORT).show();
}
mTracker = tvApplication.getTracker();
- mTvInputManagerHelper = tvApplication.getTvInputManagerHelper();
if (Features.TUNER.isEnabled(this)) {
mTvInputManagerHelper.addCallback(mTvInputCallback);
}
mTunerInputId = TunerTvInputService.getInputId(this);
mChannelDataManager = tvApplication.getChannelDataManager();
- mProgramDataManager = tvApplication.getProgramDataManager();
mProgramDataManager.addOnCurrentProgramUpdatedListener(Channel.INVALID_ID,
mOnCurrentProgramUpdatedListener);
mProgramDataManager.setPrefetchEnabled(true);
mChannelTuner = new ChannelTuner(mChannelDataManager, mTvInputManagerHelper);
mChannelTuner.addListener(mChannelTunerListener);
mChannelTuner.start();
- mPipInputManager = new PipInputManager(this, mTvInputManagerHelper, mChannelTuner);
- mPipInputManager.start();
mMemoryManageables.add(mProgramDataManager);
mMemoryManageables.add(ImageCache.getInstance());
mMemoryManageables.add(TvContentRatingCache.getInstance());
@@ -584,9 +573,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
});
- mPipView = (TunableTvView) findViewById(R.id.pip_tunable_tv_view);
- mPipView.initialize((AppLayerTvView) findViewById(R.id.pip_tv_view), true, screenHeight,
- shrunkenTvViewHeight);
+ DisplayManager displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ mDefaultRefreshRate = display.getRefreshRate();
if (!PermissionUtils.hasAccessWatchedHistory(this)) {
WatchedHistoryManager watchedHistoryManager = new WatchedHistoryManager(
@@ -594,12 +583,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
watchedHistoryManager.start();
mTvView.setWatchedHistoryManager(watchedHistoryManager);
}
- mTvViewUiManager = new TvViewUiManager(this, mTvView, mPipView,
+ mTvViewUiManager = new TvViewUiManager(this, mTvView,
(FrameLayout) findViewById(android.R.id.content), mTvOptionsManager);
- mPipView.setFixedSurfaceSize(screenWidth / 2, screenHeight / 2);
- mPipView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW);
-
+ mContentView = findViewById(android.R.id.content);
ViewGroup sceneContainer = (ViewGroup) findViewById(R.id.scene_container);
mChannelBannerView = (ChannelBannerView) getLayoutInflater().inflate(
R.layout.channel_banner, sceneContainer, false);
@@ -641,7 +628,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
});
mSearchFragment = new ProgramGuideSearchFragment();
- mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView,
+ mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mTvOptionsManager,
mKeypadChannelSwitchView, mChannelBannerView, inputBannerView,
selectInputView, sceneContainer, mSearchFragment);
@@ -692,6 +679,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mDvrConflictChecker = new ConflictChecker(this);
}
initForTest();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
}
@Override
@@ -726,9 +714,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
case PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
&& Experiments.CLOUD_EPG.get()) {
- EpgFetcher.getInstance(this).startImmediately();
- } else {
- EpgFetcher.getInstance(this).stop();
+ EpgFetcher.getInstance(this).startImmediately(false);
}
break;
}
@@ -798,14 +784,14 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
Intent notificationIntent = new Intent(this, NotificationService.class);
notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
startService(notificationIntent);
+ TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this);
}
@Override
protected void onResume() {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume start");
if (DEBUG) Log.d(TAG, "onResume()");
super.onResume();
- // Refresh the remote config, it is throttled automatically.
- TvApplication.getSingletons(this).getRemoteConfig().fetch(null);
if (!PermissionUtils.hasAccessAllEpg(this)
&& checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
!= PackageManager.PERMISSION_GRANTED) {
@@ -830,12 +816,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// visible behind.
requestVisibleBehind(true);
}
- if (Utils.hasRecordingFailedReason(getApplicationContext(),
- TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)) {
+ Set<String> failedScheduledRecordingInfoSet =
+ Utils.getFailedScheduledRecordingInfoSet(getApplicationContext());
+ if (Utils.hasRecordingFailedReason(
+ getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
+ && !failedScheduledRecordingInfoSet.isEmpty()) {
runAfterAttachedToWindow(new Runnable() {
@Override
public void run() {
- DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this);
+ DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this,
+ failedScheduledRecordingInfoSet);
}
});
}
@@ -843,7 +833,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mChannelTuner.areAllChannelsLoaded()) {
SetupUtils.getInstance(this).markNewChannelsBrowsable();
resumeTvIfNeeded();
- resumePipIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
@@ -881,6 +870,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mDvrConflictChecker != null) {
mDvrConflictChecker.start();
}
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
}
@Override
@@ -893,9 +883,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mActivityResumed = false;
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT);
mTvView.setBlockScreenType(TunableTvView.BLOCK_SCREEN_TYPE_NO_UI);
- if (mPipEnabled) {
- mTvViewUiManager.hidePipForPause();
- }
mBackKeyPressed = false;
mShowLockedChannelsTemporarily = false;
mShouldTuneToTunerChannel = false;
@@ -903,7 +890,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
+ setMediaSessionActiveAndPlaybackState(false);
}
mTracker.sendScreenView("");
} else {
@@ -962,21 +949,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTvView.setBlockScreenType(getDesiredBlockScreenType());
}
- private void resumePipIfNeeded() {
- if (mPipEnabled && !(mPipView.isPlaying() && mPipView.isShown())) {
- if (mPipInputManager.areInSamePipInput(
- mChannelTuner.getCurrentChannel(), mPipChannel)) {
- enablePipView(false, false);
- } else {
- if (!mPipView.isPlaying()) {
- startPip(false);
- } else {
- mTvViewUiManager.showPipForResume();
- }
- }
- }
- }
-
private void startTv(Uri channelUri) {
if (DEBUG) Log.d(TAG, "startTv Uri=" + channelUri);
if ((channelUri == null || !TvContract.isChannelUriForPassthroughInput(channelUri))
@@ -1027,9 +999,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- mTvView.start(mTvInputManagerHelper);
+ mTvView.start();
setVolumeByAudioFocusStatus();
- tune();
+ tune(true);
}
@Override
@@ -1072,7 +1044,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void stopAll(boolean keepVisibleBehind) {
mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION);
stopTv("stopAll()", keepVisibleBehind);
- stopPip();
}
public TvInputManagerHelper getTvInputManagerHelper() {
@@ -1145,10 +1116,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mProgramDataManager;
}
- public PipInputManager getPipInputManager() {
- return mPipInputManager;
- }
-
public TvOptionsManager getTvOptionsManager() {
return mTvOptionsManager;
}
@@ -1272,19 +1239,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mChannelBeforeShrunkenTvView = mTvView.getCurrentChannel();
mWasChannelUnblockedBeforeShrunkenByUser = mIsCurrentChannelUnblockedByUser;
mAllowedRatingBeforeShrunken = mLastAllowedRatingForCurrentChannel;
-
- if (willMainViewBeTunerInput && mChannelTuner.isCurrentChannelPassthrough()
- && mPipEnabled) {
- mPipChannelBeforeShrunkenTvView = mPipChannel;
- enablePipView(false, false);
- } else {
- mPipChannelBeforeShrunkenTvView = null;
- }
mTvViewUiManager.startShrunkenTvView();
if (showLockedChannelsTemporarily) {
mShowLockedChannelsTemporarily = true;
- checkChannelLockNeeded(mTvView);
+ checkChannelLockNeeded(mTvView, null);
}
mTvView.setBlockScreenType(getDesiredBlockScreenType());
@@ -1320,23 +1279,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
- if (mPipChannelBeforeShrunkenTvView != null) {
- enablePipView(true, false);
- mPipChannelBeforeShrunkenTvView = null;
- }
}
};
mTvViewUiManager.fadeOutTvView(tuneAction);
// Will automatically fade-in when video becomes available.
} else {
- checkChannelLockNeeded(mTvView);
+ checkChannelLockNeeded(mTvView, null);
mIsCompletingShrunkenTvView = false;
mIsCurrentChannelUnblockedByUser = mWasChannelUnblockedBeforeShrunkenByUser;
mTvView.setBlockScreenType(getDesiredBlockScreenType());
- if (mPipChannelBeforeShrunkenTvView != null) {
- enablePipView(true, false);
- mPipChannelBeforeShrunkenTvView = null;
- }
}
}
@@ -1363,7 +1314,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mChannelTuner.moveToAdjacentBrowsableChannel(true);
}
if (mTunePending) {
- tune();
+ tune(true);
}
} else {
mInputIdUnderSetup = null;
@@ -1488,11 +1439,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
});
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
- try {
- mSource = uri.getQueryParameter(Utils.PARAM_SOURCE);
- } catch (UnsupportedOperationException e) {
- // ignore this exception.
- }
// When the URI points to the programs (directory, not an individual item), go to the
// program guide. The intention here is to respond to
// "content://android.media.tv/program", not "content://android.media.tv/program/XXX".
@@ -1568,38 +1514,20 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void setVolumeByAudioFocusStatus() {
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- setVolumeByAudioFocusStatus(mTvView);
- } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
- setVolumeByAudioFocusStatus(mPipView);
- }
- }
-
- private void setVolumeByAudioFocusStatus(TunableTvView tvView) {
- SoftPreconditions.checkState(tvView == mTvView || tvView == mPipView);
- if (tvView.isPlaying()) {
+ if (mTvView.isPlaying()) {
switch (mAudioFocusStatus) {
case AudioManager.AUDIOFOCUS_GAIN:
- tvView.setStreamVolume(AUDIO_MAX_VOLUME);
+ mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- tvView.setStreamVolume(AUDIO_MIN_VOLUME);
+ mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- tvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
+ mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
break;
}
}
- if (tvView == mTvView) {
- if (mPipView != null && mPipView.isPlaying()) {
- mPipView.setStreamVolume(AUDIO_MIN_VOLUME);
- }
- } else { // tvView == mPipView
- if (mTvView != null && mTvView.isPlaying()) {
- mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
- }
- }
}
private void stopTv() {
@@ -1619,7 +1547,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
+ setMediaSessionActiveAndPlaybackState(false);
}
}
TvApplication.getSingletons(this).getMainActivityWrapper()
@@ -1632,95 +1560,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mTvView.isPlaying() && mTvView.getCurrentChannel() != null;
}
- private void startPip(final boolean fromUserInteraction) {
- if (mPipChannel == null) {
- Log.w(TAG, "PIP channel id is an invalid id.");
- return;
- }
- if (DEBUG) Log.d(TAG, "startPip() " + mPipChannel);
- mPipView.start(mTvInputManagerHelper);
- boolean success = mPipView.tuneTo(mPipChannel, null, new OnTuneListener() {
- @Override
- public void onUnexpectedStop(Channel channel) {
- Log.w(TAG, "The PIP is Unexpectedly stopped");
- enablePipView(false, false);
- }
-
- @Override
- public void onTuneFailed(Channel channel) {
- Log.w(TAG, "Fail to start the PIP during channel tuning");
- if (fromUserInteraction) {
- Toast.makeText(MainActivity.this, R.string.msg_no_pip_support,
- Toast.LENGTH_SHORT).show();
- enablePipView(false, false);
- }
- }
-
- @Override
- public void onStreamInfoChanged(StreamInfo info) {
- mTvViewUiManager.updatePipView();
- mHandler.removeCallbacks(mRestoreMainViewRunnable);
- restoreMainTvView();
- }
-
- @Override
- public void onChannelRetuned(Uri channel) {
- if (channel == null) {
- return;
- }
- Channel currentChannel =
- mChannelDataManager.getChannel(ContentUris.parseId(channel));
- if (currentChannel == null) {
- Log.e(TAG, "onChannelRetuned is called from PIP input but can't find a channel"
- + " with the URI " + channel);
- return;
- }
- if (isChannelChangeKeyDownReceived()) {
- // Ignore this message if the user is changing the channel.
- return;
- }
- mPipChannel = currentChannel;
- mPipView.setCurrentChannel(mPipChannel);
- }
-
- @Override
- public void onContentBlocked() {
- updateMediaSession();
- }
-
- @Override
- public void onContentAllowed() {
- updateMediaSession();
- }
- });
- if (!success) {
- Log.w(TAG, "Fail to start the PIP");
- return;
- }
- if (fromUserInteraction) {
- checkChannelLockNeeded(mPipView);
- }
- // Explicitly make the PIP view main to make the selected input an HDMI-CEC active source.
- mPipView.setMain();
- scheduleRestoreMainTvView();
- mTvViewUiManager.onPipStart();
- setVolumeByAudioFocusStatus();
- }
-
private void scheduleRestoreMainTvView() {
mHandler.removeCallbacks(mRestoreMainViewRunnable);
mHandler.postDelayed(mRestoreMainViewRunnable, TVVIEW_SET_MAIN_TIMEOUT_MS);
}
- private void stopPip() {
- if (DEBUG) Log.d(TAG, "stopPip");
- if (mPipView.isPlaying()) {
- mPipView.stop();
- mPipSwap = false;
- mTvViewUiManager.onPipStop();
- }
- }
-
/**
* Says {@code text} when accessibility is turned on.
*/
@@ -1735,7 +1579,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- private void tune() {
+ private void tune(boolean updateChannelBanner) {
if (DEBUG) Log.d(TAG, "tune()");
mTuneDurationTimer.start();
@@ -1825,7 +1669,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!isUnderShrunkenTvView()) {
mLastAllowedRatingForCurrentChannel = null;
}
- mHandler.removeMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE);
// For every tune, we need to inform the tuned channel or input to a user,
// if Talkback is turned on.
sendAccessibilityText(!mChannelTuner.isCurrentChannelPassthrough() ?
@@ -1852,8 +1695,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
TvApplication.getSingletons(this).getMainActivityWrapper()
.notifyCurrentChannelChange(this, channel);
}
- checkChannelLockNeeded(mTvView);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
+ // We have to provide channel here instead of using TvView's channel, because TvView's
+ // channel might be null when there's tuner conflict. In that case, TvView will resets
+ // its current channel onConnectionFailed().
+ checkChannelLockNeeded(mTvView, channel);
+ if (updateChannelBanner) {
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
+ }
if (mActivityResumed) {
// requestVisibleBehind should be called after onResume() is called. But, when
// launcher is over the TV app and the screen is turned off and on, tune() can
@@ -1897,19 +1745,18 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void updateMediaSession() {
if (getCurrentChannel() == null) {
- mMediaSession.setActive(false);
+ setMediaSessionActiveAndPlaybackState(false);
return;
}
// If the channel is blocked, display a lock and a short text on the Now Playing Card
if (mTvView.isScreenBlocked() || mTvView.getBlockedContentRating() != null) {
- setMediaSessionPlaybackState(false);
Bitmap art = BitmapFactory.decodeResource(
getResources(), R.drawable.ic_message_lock_preview);
updateMediaMetadata(
getResources().getString(R.string.channel_banner_locked_channel_title), art);
- mMediaSession.setActive(true);
+ setMediaSessionActiveAndPlaybackState(true);
return;
}
@@ -1924,13 +1771,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
cardTitleText = getCurrentChannelName();
}
updateMediaMetadata(cardTitleText, null);
- setMediaSessionPlaybackState(true);
-
if (posterArtUri == null) {
posterArtUri = TvContract.buildChannelLogoUri(getCurrentChannelId()).toString();
}
updatePosterArt(getCurrentChannel(), currentProgram, cardTitleText, null, posterArtUri);
- mMediaSession.setActive(true);
+ setMediaSessionActiveAndPlaybackState(true);
}
private void updatePosterArt(Channel currentChannel, Program currentProgram,
@@ -2017,12 +1862,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- private void setMediaSessionPlaybackState(boolean isPlaying) {
- PlaybackState.Builder builder = new PlaybackState.Builder();
- builder.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_STOPPED,
- PlaybackState.PLAYBACK_POSITION_UNKNOWN,
- isPlaying ? MEDIA_SESSION_PLAYING_SPEED : MEDIA_SESSION_STOPPED_SPEED);
- mMediaSession.setPlaybackState(builder.build());
+ private void setMediaSessionActiveAndPlaybackState(boolean isPlaying) {
+ if (isPlaying) {
+ mMediaSession.setActive(true);
+ // setMediaSessionPlaybackState has to be called after calling mMediaSession.setActive
+ // b/31933276
+ mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING);
+ } else {
+ mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED);
+ mMediaSession.setActive(false);
+ }
+
}
private void addToRecentChannels(long channelId) {
@@ -2042,33 +1892,27 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mRecentChannels;
}
- private void checkChannelLockNeeded(TunableTvView tvView) {
- Channel channel = tvView.getCurrentChannel();
- if (tvView.isPlaying() && channel != null) {
+ private void checkChannelLockNeeded(TunableTvView tvView, Channel currentChannel) {
+ if (currentChannel == null) {
+ currentChannel = tvView.getCurrentChannel();
+ }
+ if (tvView.isPlaying() && currentChannel != null) {
if (getParentalControlSettings().isParentalControlsEnabled()
- && channel.isLocked()
+ && currentChannel.isLocked()
&& !mShowLockedChannelsTemporarily
&& !(isUnderShrunkenTvView()
- && channel.equals(mChannelBeforeShrunkenTvView)
+ && currentChannel.equals(mChannelBeforeShrunkenTvView)
&& mWasChannelUnblockedBeforeShrunkenByUser)) {
- if (DEBUG) Log.d(TAG, "Channel " + channel.getId() + " is locked");
- blockScreen(tvView);
+ if (DEBUG) Log.d(TAG, "Channel " + currentChannel.getId() + " is locked");
+ blockOrUnblockScreen(tvView, true);
} else {
- unblockScreen(tvView);
+ blockOrUnblockScreen(tvView, false);
}
}
}
- private void blockScreen(TunableTvView tvView) {
- tvView.blockScreen();
- if (tvView == mTvView) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
- updateMediaSession();
- }
- }
-
- private void unblockScreen(TunableTvView tvView) {
- tvView.unblockScreen();
+ private void blockOrUnblockScreen(TunableTvView tvView, boolean blockOrUnblock) {
+ tvView.blockOrUnblockScreen(blockOrUnblock);
if (tvView == mTvView) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
updateMediaSession();
@@ -2089,36 +1933,59 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) {
if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")");
+ if (isChannelChangeKeyDownReceived() && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE
+ && reason != UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) {
+ // Tuning is still ongoing, no need to update banner for other reasons
+ return;
+ }
if (!mChannelTuner.isCurrentChannelPassthrough()) {
int lockType = ChannelBannerView.LOCK_NONE;
- if (mTvView.isScreenBlocked()) {
+ if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) {
+ if (getParentalControlSettings().isParentalControlsEnabled()
+ && getCurrentChannel().isLocked()) {
+ lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
+ } else {
+ // Do not show detailed program information while fast-tuning.
+ lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
+ }
+ } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE) {
+ if (getParentalControlSettings().isParentalControlsEnabled()) {
+ if (getCurrentChannel().isLocked()) {
+ lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
+ } else {
+ // If parental control is turned on,
+ // assumes that program is locked by default and waits for onContentAllowed.
+ lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
+ }
+ }
+ } else if (mTvView.isScreenBlocked()) {
lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
} else if (mTvView.getBlockedContentRating() != null
|| (getParentalControlSettings().isParentalControlsEnabled()
- && !mTvView.isVideoAvailable())) {
+ && !mTvView.isVideoOrAudioAvailable())) {
// If the parental control is enabled, do not show the program detail until the
// video becomes available.
lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
}
- if (lockType == ChannelBannerView.LOCK_NONE) {
- if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) {
- // Do not show detailed program information while fast-tuning.
- lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
- } else if (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
- && getParentalControlSettings().isParentalControlsEnabled()) {
- // If parental control is turned on,
- // assumes that program is locked by default and waits for onContentAllowed.
- lockType = ChannelBannerView.LOCK_PROGRAM_DETAIL;
- }
- }
// If lock type is not changed, we don't need to update channel banner by parental
// control.
- if (!mChannelBannerView.setLockType(lockType)
+ int previousLockType = mChannelBannerView.setLockType(lockType);
+ if (previousLockType == lockType
&& reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) {
return;
+ } else if (reason == UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO) {
+ mChannelBannerView.updateStreamInfo(mTvView);
+ // If parental control is enabled, we shows program description when the video is
+ // available, instead of tuning. Therefore we need to check it here if the program
+ // description is previously hidden by parental control.
+ if (previousLockType == ChannelBannerView.LOCK_PROGRAM_DETAIL &&
+ lockType != ChannelBannerView.LOCK_PROGRAM_DETAIL) {
+ mChannelBannerView.updateViews(false);
+ }
+ } else {
+ mChannelBannerView.updateViews(reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
+ || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST);
}
-
- mChannelBannerView.updateViews(mTvView);
}
boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW
|| reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
@@ -2197,7 +2064,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (bestTrack != null) {
String selectedTrack = getSelectedTrack(TvTrackInfo.TYPE_AUDIO);
if (!bestTrack.getId().equals(selectedTrack)) {
- selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack);
+ selectTrack(TvTrackInfo.TYPE_AUDIO, bestTrack, UNDEFINED_TRACK_INDEX);
} else {
mTvOptionsManager.onMultiAudioChanged(
Utils.getMultiAudioString(this, bestTrack, false));
@@ -2210,7 +2077,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void applyClosedCaption() {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks == null) {
- mTvOptionsManager.onClosedCaptionsChanged(null);
+ mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
return;
}
@@ -2219,17 +2086,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
String selectedTrackId = getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE);
TvTrackInfo alternativeTrack = null;
+ int alternativeTrackIndex = UNDEFINED_TRACK_INDEX;
if (enabled) {
String language = mCaptionSettings.getLanguage();
String trackId = mCaptionSettings.getTrackId();
- for (TvTrackInfo track : tracks) {
+ for (int i = 0; i < tracks.size(); i++) {
+ TvTrackInfo track = tracks.get(i);
if (Utils.isEqualLanguage(track.getLanguage(), language)) {
if (track.getId().equals(trackId)) {
if (!track.getId().equals(selectedTrackId)) {
- selectTrack(TvTrackInfo.TYPE_SUBTITLE, track);
+ selectTrack(TvTrackInfo.TYPE_SUBTITLE, track, i);
} else {
// Already selected. Update the option string only.
- mTvOptionsManager.onClosedCaptionsChanged(track);
+ mTvOptionsManager.onClosedCaptionsChanged(track, i);
}
if (DEBUG) {
Log.d(TAG, "Subtitle Track Selected {id=" + track.getId()
@@ -2238,14 +2107,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
} else if (alternativeTrack == null) {
alternativeTrack = track;
+ alternativeTrackIndex = i;
}
}
}
if (alternativeTrack != null) {
if (!alternativeTrack.getId().equals(selectedTrackId)) {
- selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack);
+ selectTrack(TvTrackInfo.TYPE_SUBTITLE, alternativeTrack, alternativeTrackIndex);
} else {
- mTvOptionsManager.onClosedCaptionsChanged(alternativeTrack);
+ mTvOptionsManager
+ .onClosedCaptionsChanged(alternativeTrack, alternativeTrackIndex);
}
if (DEBUG) {
Log.d(TAG, "Subtitle Track Selected {id=" + alternativeTrack.getId()
@@ -2255,11 +2126,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
if (selectedTrackId != null) {
- selectTrack(TvTrackInfo.TYPE_SUBTITLE, null);
+ selectTrack(TvTrackInfo.TYPE_SUBTITLE, null, UNDEFINED_TRACK_INDEX);
if (DEBUG) Log.d(TAG, "Subtitle Track Unselected");
return;
}
- mTvOptionsManager.onClosedCaptionsChanged(null);
+ mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
}
/**
@@ -2274,12 +2145,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- public void showSearchActivity() {
- // HACK: Once we moved the window layer to TYPE_APPLICATION_SUB_PANEL,
- // the voice button doesn't work. So we directly call the voice action.
- SearchManagerHelper.getInstance(this).launchAssistAction();
- }
-
public void showProgramGuideSearchFragment() {
getFragmentManager().beginTransaction().replace(R.id.fragment_container, mSearchFragment)
.addToBackStack(null).commit();
@@ -2295,13 +2160,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy()");
- SideFragment.releasePreloadedRecycledViews();
+ SideFragment.releaseRecycledViewPool();
+ ViewCache.getInstance().clear();
if (mTvView != null) {
mTvView.release();
}
- if (mPipView != null) {
- mPipView.release();
- }
if (mChannelTuner != null) {
mChannelTuner.removeListener(mChannelTunerListener);
mChannelTuner.stop();
@@ -2314,9 +2177,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mProgramDataManager.setPrefetchEnabled(false);
}
}
- if (mPipInputManager != null) {
- mPipInputManager.stop();
- }
if (mOverlayManager != null) {
mOverlayManager.release();
}
@@ -2340,8 +2200,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mChannelStatusRecurringRunner.stop();
mChannelStatusRecurringRunner = null;
}
- if (mTvInputManagerHelper != null && Features.TUNER.isEnabled(this)) {
- mTvInputManagerHelper.removeCallback(mTvInputCallback);
+ if (mTvInputManagerHelper != null) {
+ mTvInputManagerHelper.clearTvInputLabels();
+ if (Features.TUNER.isEnabled(this)) {
+ mTvInputManagerHelper.removeCallback(mTvInputCallback);
+ }
}
super.onDestroy();
}
@@ -2410,7 +2273,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
* G debug: refresh cloud epg
* I KEYCODE_TV_INPUT
* O debug: show display mode option
- * P debug: togglePipView
* S KEYCODE_CAPTIONS: select subtitle
* W debug: toggle screen size
* V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec
@@ -2422,8 +2284,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
finishChannelChangeIfNeeded();
if (event.getKeyCode() == KeyEvent.KEYCODE_SEARCH) {
- showSearchActivity();
- return true;
+ // Prevent MainActivity from being closed by onVisibleBehindCanceled()
+ mOtherActivityLaunched = true;
+ return false;
}
switch (mOverlayManager.onKeyUp(keyCode, event)) {
case KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY:
@@ -2472,7 +2335,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
- if (!mTvView.isVideoAvailable()
+ if (!mTvView.isVideoOrAudioAvailable()
&& mTvView.getVideoUnavailableReason()
== TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) {
DvrUiHelper.startSchedulesActivityForTuneConflict(this,
@@ -2491,7 +2354,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
public void done(boolean success) {
if (success) {
- unblockScreen(mTvView);
+ blockOrUnblockScreen(mTvView, false);
mIsCurrentChannelUnblockedByUser = true;
}
}
@@ -2578,22 +2441,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
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();
- }
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(this,
+ currentChannel.getInputId(), new Runnable() {
+ @Override
+ public void run() {
+ DvrUiHelper.requestRecordingCurrentProgram(
+ MainActivity.this,
+ currentChannel, program, false);
+ }
+ });
}
} else {
DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(),
@@ -2643,10 +2501,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
return true;
}
- case KeyEvent.KEYCODE_P: {
- togglePipView();
- return true;
- }
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT: {
mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
@@ -2681,22 +2535,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
@Override
- public void onBackPressed() {
- // The activity should be returned to the caller of this activity
- // when the mSource is not null.
- if (!mOverlayManager.getSideFragmentManager().isActive() && isPlaying()
- && mSource == null) {
- // If back key would exit TV app,
- // show McLauncher instead so we can get benefit of McLauncher's shyMode.
- Intent startMain = new Intent(Intent.ACTION_MAIN);
- startMain.addCategory(Intent.CATEGORY_HOME);
- startActivity(startMain);
- } else {
- super.onBackPressed();
- }
- }
-
- @Override
public void onUserInteraction() {
super.onUserInteraction();
if (mOverlayManager != null) {
@@ -2725,65 +2563,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- public void togglePipView() {
- enablePipView(!mPipEnabled, true);
- mOverlayManager.getMenu().update();
- }
-
- public boolean isPipEnabled() {
- return mPipEnabled;
- }
-
- public void tuneToChannelForPip(Channel channel) {
- if (!mPipEnabled) {
- throw new IllegalStateException("tuneToChannelForPip is called when PIP is off");
- }
- if (mPipChannel.equals(channel)) {
- return;
- }
- mPipChannel = channel;
- startPip(true);
- }
-
- private void enablePipView(boolean enable, boolean fromUserInteraction) {
- if (enable == mPipEnabled) {
- return;
- }
- if (enable) {
- List<PipInput> pipAvailableInputs = mPipInputManager.getPipInputList(true);
- if (pipAvailableInputs.isEmpty()) {
- Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
- .show();
- return;
- }
- // TODO: choose the last pip input.
- Channel pipChannel = pipAvailableInputs.get(0).getChannel();
- if (pipChannel != null) {
- mPipEnabled = true;
- mPipChannel = pipChannel;
- startPip(fromUserInteraction);
- mTvViewUiManager.restorePipSize();
- mTvViewUiManager.restorePipLayout();
- mTvOptionsManager.onPipChanged(mPipEnabled);
- } else {
- Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
- .show();
- }
- } else {
- mPipEnabled = false;
- mPipChannel = null;
- // Recover the stream volume of the main TV view, if needed.
- if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
- setVolumeByAudioFocusStatus(mTvView);
- mPipSound = TvSettings.PIP_SOUND_MAIN;
- mTvOptionsManager.onPipSoundChanged(mPipSound);
- }
- stopPip();
- mTvViewUiManager.restoreDisplayMode(false);
- mTvOptionsManager.onPipChanged(mPipEnabled);
- }
- }
-
private boolean isChannelChangeKeyDownReceived() {
return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED)
|| mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED);
@@ -2811,10 +2590,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (SystemProperties.LOG_KEYEVENT.getValue()) {
Log.d(TAG, "dispatchKeyEventToSession(" + event + ")");
}
- if (mPipEnabled && mChannelTuner.isCurrentChannelPassthrough()) {
- // If PIP is enabled, key events will be used by UI.
- return false;
- }
boolean handled = false;
if (mTvView != null) {
handled = mTvView.dispatchKeyEvent(event);
@@ -2832,21 +2607,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private boolean isKeyEventBlocked() {
- // If the current channel is passthrough channel without a PIP view,
- // we always don't handle the key events in TV activity. Instead, the key event will
- // be handled by the passthrough TV input.
- return mChannelTuner.isCurrentChannelPassthrough() && !mPipEnabled;
+ // If the current channel is a passthrough channel, we don't handle the key events in TV
+ // activity. Instead, the key event will be handled by the passthrough TV input.
+ return mChannelTuner.isCurrentChannelPassthrough();
}
private void tuneToLastWatchedChannelForTunerInput() {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
return;
}
- if (mPipEnabled) {
- if (!mPipChannel.isPassthrough()) {
- enablePipView(false, true);
- }
- }
stopTv();
startTv(null);
}
@@ -2857,16 +2626,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTvView.reset();
}
} else {
- if (mPipEnabled && mPipInputManager.areInSamePipInput(channel, mPipChannel)) {
- enablePipView(false, true);
- }
if (!mTvView.isPlaying()) {
startTv(channel.getUri());
} else if (channel.equals(mTvView.getCurrentChannel())) {
updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
+ } else if (channel == mChannelTuner.getCurrentChannel()) {
+ // Channel banner is already updated in moveToAdjacentChannel
+ tune(false);
} else if (mChannelTuner.moveToChannel(channel)) {
// Channel banner would be updated inside of tune.
- tune();
+ tune(true);
} else {
showSettingsFragment();
}
@@ -2888,90 +2657,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- public Channel getPipChannel() {
- return mPipChannel;
- }
-
- /**
- * Swap the main and the sub screens while in the PIP mode.
- */
- public void swapPip() {
- if (!mPipEnabled || mTvView == null || mPipView == null) {
- Log.e(TAG, "swapPip() - not in PIP");
- mPipSwap = false;
- return;
- }
-
- Channel channel = mTvView.getCurrentChannel();
- boolean tvViewBlocked = mTvView.isScreenBlocked();
- boolean pipViewBlocked = mPipView.isScreenBlocked();
- if (channel == null || !mTvView.isPlaying()) {
- // If the TV view is not currently playing or its current channel is null, swapping here
- // basically means disabling the PIP mode and getting back to the full screen since
- // there's no point of keeping a blank PIP screen at the bottom which is not tune-able.
- enablePipView(false, true);
- mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_DEFAULT);
- mPipSwap = false;
- return;
- }
-
- // Reset the TV view and tune the PIP view to the previous channel of the TV view.
- mTvView.reset();
- mPipView.reset();
- Channel oldPipChannel = mPipChannel;
- tuneToChannelForPip(channel);
- if (tvViewBlocked) {
- mPipView.blockScreen();
- } else {
- mPipView.unblockScreen();
- }
-
- if (oldPipChannel != null) {
- // Tune the TV view to the previous PIP channel.
- tuneToChannel(oldPipChannel);
- }
- if (pipViewBlocked) {
- mTvView.blockScreen();
- } else {
- mTvView.unblockScreen();
- }
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- setVolumeByAudioFocusStatus(mTvView);
- } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
- setVolumeByAudioFocusStatus(mPipView);
- }
- mPipSwap = !mPipSwap;
- mTvOptionsManager.onPipSwapChanged(mPipSwap);
- }
-
- /**
- * Toggle where the sound is coming from when the user is watching the PIP.
- */
- public void togglePipSoundMode() {
- if (!mPipEnabled || mTvView == null || mPipView == null) {
- Log.e(TAG, "togglePipSoundMode() - not in PIP");
- return;
- }
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- setVolumeByAudioFocusStatus(mPipView);
- mPipSound = TvSettings.PIP_SOUND_PIP_WINDOW;
- } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
- setVolumeByAudioFocusStatus(mTvView);
- mPipSound = TvSettings.PIP_SOUND_MAIN;
- }
- restoreMainTvView();
- mTvOptionsManager.onPipSoundChanged(mPipSound);
- }
-
/**
* Set the main TV view which holds HDMI-CEC active source based on the sound mode
*/
private void restoreMainTvView() {
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- mTvView.setMain();
- } else { // mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW
- mPipView.setMain();
- }
+ mTvView.setMain();
}
@Override
@@ -2981,9 +2671,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
mAudioManager.abandonAudioFocus(this);
if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
+ setMediaSessionActiveAndPlaybackState(false);
}
- stopPip();
mVisibleBehind = false;
if (!mOtherActivityLaunched && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
// Workaround: in M, onStop is not called, even though it should be called after
@@ -3012,13 +2701,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mTvView.getSelectedTrack(type);
}
- private void selectTrack(int type, TvTrackInfo track) {
+ private void selectTrack(int type, TvTrackInfo track, int trackIndex) {
mTvView.selectTrack(type, track == null ? null : track.getId());
if (type == TvTrackInfo.TYPE_AUDIO) {
mTvOptionsManager.onMultiAudioChanged(track == null ? null :
Utils.getMultiAudioString(this, track, false));
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
- mTvOptionsManager.onClosedCaptionsChanged(track);
+ mTvOptionsManager.onClosedCaptionsChanged(track, trackIndex);
}
}
@@ -3163,6 +2852,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mActivityStarted) {
initAnimations();
initSideFragments();
+ initMenuItemViews();
}
}
}, LAZY_INITIALIZATION_DELAY);
@@ -3174,7 +2864,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void initSideFragments() {
- SideFragment.preloadRecycledViews(this);
+ SideFragment.preloadItemViews(this);
+ }
+
+ private void initMenuItemViews() {
+ mOverlayManager.getMenu().preloadItemViews();
}
@Override
@@ -3207,10 +2901,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
sendMessageDelayed(Message.obtain(msg), getDelay(startTime));
mainActivity.moveToAdjacentChannel(true, true);
break;
- case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE:
- mainActivity.updateChannelBannerAndShowIfNeeded(
- UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
- break;
}
}
@@ -3225,14 +2915,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private class MyOnTuneListener implements OnTuneListener {
boolean mUnlockAllowedRatingBeforeShrunken = true;
boolean mWasUnderShrunkenTvView;
- long mStreamInfoUpdateTimeThresholdMs;
Channel mChannel;
public MyOnTuneListener() { }
private void onTune(Channel channel, boolean wasUnderShrukenTvView) {
- mStreamInfoUpdateTimeThresholdMs =
- System.currentTimeMillis() + FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS;
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.MyOnTuneListener.onTune");
mChannel = channel;
mWasUnderShrunkenTvView = wasUnderShrukenTvView;
}
@@ -3258,20 +2946,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTracker.sendChannelTuneTime(info.getCurrentChannel(),
mTuneDurationTimer.reset());
}
- // If updateChannelBanner() is called without delay, the stream info seems flickering
- // when the channel is quickly changed.
- if (!mHandler.hasMessages(MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE)
- && info.isVideoAvailable()) {
- if (System.currentTimeMillis() > mStreamInfoUpdateTimeThresholdMs) {
- updateChannelBannerAndShowIfNeeded(
- UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
- } else {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE),
- mStreamInfoUpdateTimeThresholdMs - System.currentTimeMillis());
- }
+ if (info.isVideoOrAudioAvailable() && mChannel == getCurrentChannel()) {
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO);
}
-
applyDisplayRefreshRate(info.getVideoFrameRate());
mTvViewUiManager.updateTvView();
applyMultiAudio();
@@ -3308,6 +2985,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
public void onContentBlocked() {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "MainActivity.MyOnTuneListener.onContentBlocked removes timer");
+ Debug.removeTimer(Debug.TAG_START_UP_TIMER);
mTuneDurationTimer.reset();
TvContentRating rating = mTvView.getBlockedContentRating();
// When tuneTo was called while TV view was shrunken, if the channel id is the same
@@ -3319,8 +2999,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
}
- mChannelBannerView.setBlockingContentRating(rating);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ if (!isChannelChangeKeyDownReceived()) {
+ mChannelBannerView.setBlockingContentRating(rating);
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ }
mTvViewUiManager.fadeInTvView();
}
@@ -3329,8 +3011,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!isUnderShrunkenTvView()) {
mUnlockAllowedRatingBeforeShrunken = false;
}
- mChannelBannerView.setBlockingContentRating(null);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ if (!isChannelChangeKeyDownReceived()) {
+ mChannelBannerView.setBlockingContentRating(null);
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 8a263a26..6459693b 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -44,64 +44,72 @@ public class SetupPassthroughActivity extends Activity {
private TvInputInfo mTvInputInfo;
private Intent mActivityAfterCompletion;
+ private boolean mEpgFetcherDuringScan;
@Override
public void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
- Intent intent = getIntent();
- SoftPreconditions.checkState(
- intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP));
ApplicationSingletons appSingletons = TvApplication.getSingletons(this);
TvInputManagerHelper inputManager = appSingletons.getTvInputManagerHelper();
+ Intent intent = getIntent();
String inputId = intent.getStringExtra(TvCommonConstants.EXTRA_INPUT_ID);
mTvInputInfo = inputManager.getTvInputInfo(inputId);
- if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo);
- if (mTvInputInfo == null) {
- Log.w(TAG, "There is no input with the ID " + inputId + ".");
- finish();
- return;
- }
- Intent setupIntent = intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT);
- if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent);
- if (setupIntent == null) {
- Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup.");
- finish();
- return;
- }
- SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName);
mActivityAfterCompletion = intent.getParcelableExtra(
TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION);
- if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion);
- // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during
- // setupIntent.putExtras(intent.getExtras()).
- Bundle extras = intent.getExtras();
- extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT);
- setupIntent.putExtras(extras);
- try {
- startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
- } catch (ActivityNotFoundException e) {
- Log.e(TAG, "Can't find activity: " + setupIntent.getComponent());
- finish();
- return;
- }
- if (Utils.isInternalTvInput(this, mTvInputInfo.getId()) && Experiments.CLOUD_EPG.get()) {
- EpgFetcher.getInstance(this).stop();
+ boolean needToFetchEpg = Utils.isInternalTvInput(this, mTvInputInfo.getId())
+ && Experiments.CLOUD_EPG.get();
+ if (needToFetchEpg) {
+ // In case when the activity is restored, this flag should be restored as well.
+ mEpgFetcherDuringScan = true;
}
- }
-
- @Override
- protected void onDestroy() {
- if (mTvInputInfo != null && Utils.isInternalTvInput(this, mTvInputInfo.getId())
- && Experiments.CLOUD_EPG.get()) {
- EpgFetcher.getInstance(this).start();
+ if (savedInstanceState == null) {
+ SoftPreconditions.checkState(
+ intent.getAction().equals(TvCommonConstants.INTENT_ACTION_INPUT_SETUP));
+ if (DEBUG) Log.d(TAG, "TvInputId " + inputId + " / TvInputInfo " + mTvInputInfo);
+ if (mTvInputInfo == null) {
+ Log.w(TAG, "There is no input with the ID " + inputId + ".");
+ finish();
+ return;
+ }
+ Intent setupIntent =
+ intent.getExtras().getParcelable(TvCommonConstants.EXTRA_SETUP_INTENT);
+ if (DEBUG) Log.d(TAG, "Setup activity launch intent: " + setupIntent);
+ if (setupIntent == null) {
+ Log.w(TAG, "The input (" + mTvInputInfo.getId() + ") doesn't have setup.");
+ finish();
+ return;
+ }
+ SetupUtils.grantEpgPermission(this, mTvInputInfo.getServiceInfo().packageName);
+ if (DEBUG) Log.d(TAG, "Activity after completion " + mActivityAfterCompletion);
+ // If EXTRA_SETUP_INTENT is not removed, an infinite recursion happens during
+ // setupIntent.putExtras(intent.getExtras()).
+ Bundle extras = intent.getExtras();
+ extras.remove(TvCommonConstants.EXTRA_SETUP_INTENT);
+ setupIntent.putExtras(extras);
+ try {
+ startActivityForResult(setupIntent, REQUEST_START_SETUP_ACTIVITY);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Can't find activity: " + setupIntent.getComponent());
+ finish();
+ return;
+ }
+ if (needToFetchEpg) {
+ EpgFetcher.getInstance(this).onChannelScanStarted();
+ }
}
- super.onDestroy();
}
@Override
public void onActivityResult(int requestCode, final int resultCode, final Intent data) {
+ if (DEBUG) Log.d(TAG, "onActivityResult");
boolean setupComplete = requestCode == REQUEST_START_SETUP_ACTIVITY
&& resultCode == Activity.RESULT_OK;
+ // Tells EpgFetcher that channel source setup is finished.
+ if (mEpgFetcherDuringScan) {
+ EpgFetcher.getInstance(this).onChannelScanFinished();
+ mEpgFetcherDuringScan = false;
+ }
if (!setupComplete) {
setResult(resultCode, data);
finish();
@@ -122,4 +130,4 @@ public class SetupPassthroughActivity extends Activity {
}
});
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index 2d6d45c4..e1024705 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -887,10 +887,11 @@ public class TimeShiftManager {
}
long fetchStartTimeMs = Utils.floorTime(startTimeMs, MAX_DUMMY_PROGRAM_DURATION);
- boolean needToLoad = addDummyPrograms(fetchStartTimeMs,
- endTimeMs + PREFETCH_DURATION_FOR_NEXT);
+ long fetchEndTimeMs = Utils.ceilTime(endTimeMs + PREFETCH_DURATION_FOR_NEXT,
+ MAX_DUMMY_PROGRAM_DURATION);
+ boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs);
if (needToLoad) {
- Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs);
+ Range<Long> period = Range.create(fetchStartTimeMs, fetchEndTimeMs);
mProgramLoadQueue.add(period);
startTaskIfNeeded();
}
@@ -1012,7 +1013,7 @@ public class TimeShiftManager {
for (int i = 0, j = 0; i < mPrograms.size() && j < loadedPrograms.size(); ++j) {
Program loadedProgram = loadedPrograms.get(j);
// Skip previous programs.
- while (program.getEndTimeUtcMillis() < loadedProgram.getStartTimeUtcMillis()) {
+ while (program.getEndTimeUtcMillis() <= loadedProgram.getStartTimeUtcMillis()) {
// Reached end of mPrograms.
if (++i == mPrograms.size()) {
return;
diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java
index 0e18a259..159df7b6 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -37,6 +37,7 @@ import android.util.Log;
import android.view.KeyEvent;
import com.android.tv.analytics.Analytics;
+import com.android.tv.util.Debug;
import com.android.tv.analytics.StubAnalytics;
import com.android.tv.analytics.StubAnalytics;
import com.android.tv.analytics.Tracker;
@@ -53,10 +54,10 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
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.dvr.recorder.DvrRecordingService;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import com.android.tv.tuner.util.TunerInputInfoUtils;
@@ -106,6 +107,8 @@ public class TvApplication extends Application implements ApplicationSingletons
@Override
public void onCreate() {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).start();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate start");
super.onCreate();
SharedPreferencesUtils.initialize(this, new Runnable() {
@Override
@@ -149,13 +152,13 @@ public class TvApplication extends Application implements ApplicationSingletons
mAnalytics = StubAnalytics.getInstance(this);
}
mTracker = mAnalytics.getDefaultTracker();
- mTvInputManagerHelper = new TvInputManagerHelper(this);
- mTvInputManagerHelper.start();
+ getTvInputManagerHelper();
// In SetupFragment, transitions are set in the constructor. Because the fragment can be
// created in Activity.onCreate() by the framework, SetupAnimationHelper should be
// initialized here before Activity.onCreate() is called.
SetupAnimationHelper.initialize(this);
Log.i(TAG, "Started Live TV " + mVersionName);
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TvApplication.onCreate end");
}
private void setCurrentRunningProcess(boolean isMainProcess) {
@@ -167,8 +170,10 @@ public class TvApplication extends Application implements ApplicationSingletons
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess);
}
+ // Fetch remote config
+ getSingletons(this).getRemoteConfig().fetch(null);
if (mRunningInMainProcess) {
- mTvInputManagerHelper.addCallback(new TvInputCallback() {
+ getTvInputManagerHelper().addCallback(new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
if (Features.TUNER.isEnabled(TvApplication.this) && TextUtils.equals(inputId,
@@ -268,7 +273,7 @@ public class TvApplication extends Application implements ApplicationSingletons
@Override
public ChannelDataManager getChannelDataManager() {
if (mChannelDataManager == null) {
- mChannelDataManager = new ChannelDataManager(this, mTvInputManagerHelper);
+ mChannelDataManager = new ChannelDataManager(this, getTvInputManagerHelper());
mChannelDataManager.start();
}
return mChannelDataManager;
@@ -314,6 +319,10 @@ public class TvApplication extends Application implements ApplicationSingletons
*/
@Override
public TvInputManagerHelper getTvInputManagerHelper() {
+ if (mTvInputManagerHelper == null) {
+ mTvInputManagerHelper = new TvInputManagerHelper(this);
+ mTvInputManagerHelper.start();
+ }
return mTvInputManagerHelper;
}
diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java
index 7871cbe7..493e039c 100644
--- a/src/com/android/tv/TvOptionsManager.java
+++ b/src/com/android/tv/TvOptionsManager.java
@@ -18,14 +18,13 @@ package com.android.tv;
import android.content.Context;
import android.media.tv.TvTrackInfo;
+import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.android.tv.data.DisplayMode;
-import com.android.tv.util.TvSettings;
-import com.android.tv.util.TvSettings.PipLayout;
-import com.android.tv.util.TvSettings.PipSize;
-import com.android.tv.util.TvSettings.PipSound;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
/**
@@ -33,39 +32,34 @@ import java.util.Locale;
* captions and display mode. Can be also used to create MenuAction items to control such options.
*/
public class TvOptionsManager {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({OPTION_CLOSED_CAPTIONS, OPTION_DISPLAY_MODE, OPTION_SYSTEMWIDE_PIP, OPTION_MULTI_AUDIO,
+ OPTION_MORE_CHANNELS, OPTION_DEVELOPER, OPTION_SETTINGS})
+ public @interface OptionType {}
public static final int OPTION_CLOSED_CAPTIONS = 0;
public static final int OPTION_DISPLAY_MODE = 1;
- public static final int OPTION_IN_APP_PIP = 2;
- public static final int OPTION_SYSTEMWIDE_PIP = 3;
- public static final int OPTION_MULTI_AUDIO = 4;
- public static final int OPTION_MORE_CHANNELS = 5;
- public static final int OPTION_DEVELOPER = 6;
- public static final int OPTION_SETTINGS = 7;
-
- public static final int OPTION_PIP_INPUT = 100;
- public static final int OPTION_PIP_SWAP = 101;
- public static final int OPTION_PIP_SOUND = 102;
- public static final int OPTION_PIP_LAYOUT = 103 ;
- public static final int OPTION_PIP_SIZE = 104;
+ public static final int OPTION_SYSTEMWIDE_PIP = 2;
+ public static final int OPTION_MULTI_AUDIO = 3;
+ public static final int OPTION_MORE_CHANNELS = 4;
+ public static final int OPTION_DEVELOPER = 5;
+ public static final int OPTION_SETTINGS = 6;
private final Context mContext;
private final SparseArray<OptionChangedListener> mOptionChangedListeners = new SparseArray<>();
private String mClosedCaptionsLanguage;
private int mDisplayMode;
- private boolean mPip;
private String mMultiAudio;
- private String mPipInput;
- private boolean mPipSwap;
- @PipSound private int mPipSound;
- @PipLayout private int mPipLayout;
- @PipSize private int mPipSize;
public TvOptionsManager(Context context) {
mContext = context;
}
- public String getOptionString(int option) {
+ /**
+ * Returns a suitable displayed string for the given option type under current settings.
+ * @param option the type of option, should be one of {@link OptionType}.
+ */
+ public String getOptionString(@OptionType int option) {
switch (option) {
case OPTION_CLOSED_CAPTIONS:
if (mClosedCaptionsLanguage == null) {
@@ -77,101 +71,48 @@ public class TvOptionsManager {
.isDisplayModeAvailable(mDisplayMode)
? DisplayMode.getLabel(mDisplayMode, mContext)
: DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext);
- case OPTION_IN_APP_PIP:
- return mContext.getString(
- mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off);
case OPTION_MULTI_AUDIO:
return mMultiAudio;
- case OPTION_PIP_INPUT:
- return mPipInput;
- case OPTION_PIP_SWAP:
- return mContext.getString(mPipSwap ? R.string.pip_options_item_swap_on
- : R.string.pip_options_item_swap_off);
- case OPTION_PIP_SOUND:
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- return mContext.getString(R.string.pip_options_item_sound_main);
- } else if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
- return mContext.getString(R.string.pip_options_item_sound_pip_window);
- }
- break;
- case OPTION_PIP_LAYOUT:
- if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_RIGHT) {
- return mContext.getString(R.string.pip_options_item_layout_bottom_right);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_RIGHT) {
- return mContext.getString(R.string.pip_options_item_layout_top_right);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_LEFT) {
- return mContext.getString(R.string.pip_options_item_layout_top_left);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_LEFT) {
- return mContext.getString(R.string.pip_options_item_layout_bottom_left);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- return mContext.getString(R.string.pip_options_item_layout_side_by_side);
- }
- break;
- case OPTION_PIP_SIZE:
- if (mPipSize == TvSettings.PIP_SIZE_BIG) {
- return mContext.getString(R.string.pip_options_item_size_big);
- } else if (mPipSize == TvSettings.PIP_SIZE_SMALL) {
- return mContext.getString(R.string.pip_options_item_size_small);
- }
- break;
}
return "";
}
- public void onClosedCaptionsChanged(TvTrackInfo track) {
- mClosedCaptionsLanguage = (track == null) ? null
- : (track.getLanguage() != null) ? track.getLanguage()
- : mContext.getString(R.string.default_language);
+ /**
+ * Handles changing selection of closed caption.
+ */
+ public void onClosedCaptionsChanged(TvTrackInfo track, int trackIndex) {
+ mClosedCaptionsLanguage = (track == null) ?
+ null : (track.getLanguage() != null) ? track.getLanguage()
+ : mContext.getString(R.string.closed_caption_unknown_language, trackIndex + 1);
notifyOptionChanged(OPTION_CLOSED_CAPTIONS);
}
+ /**
+ * Handles changing selection of display mode.
+ */
public void onDisplayModeChanged(int displayMode) {
mDisplayMode = displayMode;
notifyOptionChanged(OPTION_DISPLAY_MODE);
}
- public void onPipChanged(boolean pip) {
- mPip = pip;
- notifyOptionChanged(OPTION_IN_APP_PIP);
- }
-
+ /**
+ * Handles changing selection of multi-audio.
+ */
public void onMultiAudioChanged(String multiAudio) {
mMultiAudio = multiAudio;
notifyOptionChanged(OPTION_MULTI_AUDIO);
}
- public void onPipInputChanged(String pipInput) {
- mPipInput = pipInput;
- notifyOptionChanged(OPTION_PIP_INPUT);
- }
-
- public void onPipSwapChanged(boolean pipSwap) {
- mPipSwap = pipSwap;
- notifyOptionChanged(OPTION_PIP_SWAP);
- }
-
- public void onPipSoundChanged(@PipSound int pipSound) {
- mPipSound = pipSound;
- notifyOptionChanged(OPTION_PIP_SOUND);
- }
-
- public void onPipLayoutChanged(@PipLayout int pipLayout) {
- mPipLayout = pipLayout;
- notifyOptionChanged(OPTION_PIP_LAYOUT);
- }
-
- public void onPipSizeChanged(@PipSize int pipSize) {
- mPipSize = pipSize;
- notifyOptionChanged(OPTION_PIP_SIZE);
- }
-
- private void notifyOptionChanged(int option) {
+ private void notifyOptionChanged(@OptionType int option) {
OptionChangedListener listener = mOptionChangedListeners.get(option);
if (listener != null) {
- listener.onOptionChanged(getOptionString(option));
+ listener.onOptionChanged(option, getOptionString(option));
}
}
+ /**
+ * Sets listeners to changes of the given option type.
+ */
public void setOptionChangedListener(int option, OptionChangedListener listener) {
mOptionChangedListeners.put(option, listener);
}
@@ -180,6 +121,6 @@ public class TvOptionsManager {
* An interface used to monitor option changes.
*/
public interface OptionChangedListener {
- void onOptionChanged(String newOption);
+ void onOptionChanged(@OptionType int optionType, String newString);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 30f84236..4da56311 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -52,6 +52,16 @@ public final class Channel {
public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3;
/**
+ * Compares the channel numbers of channels which belong to the same input.
+ */
+ public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() {
+ @Override
+ public int compare(Channel lhs, Channel rhs) {
+ return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ }
+ };
+
+ /**
* When a TIS doesn't provide any information about app link, and it doesn't have a leanback
* launch intent, there will be no app link card for the TIS.
*/
@@ -87,9 +97,15 @@ public final class Channel {
TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
};
/**
+ * Channel number delimiter between major and minor parts.
+ */
+ public static final char CHANNEL_NUMBER_DELIMITER = '-';
+
+ /**
* Creates {@code Channel} object from cursor.
*
* <p>The query that created the cursor MUST use {@link #PROJECTION}
@@ -103,7 +119,7 @@ public final class Channel {
channel.mPackageName = Utils.intern(cursor.getString(index++));
channel.mInputId = Utils.intern(cursor.getString(index++));
channel.mType = Utils.intern(cursor.getString(index++));
- channel.mDisplayNumber = cursor.getString(index++);
+ channel.mDisplayNumber = normalizeDisplayNumber(cursor.getString(index++));
channel.mDisplayName = cursor.getString(index++);
channel.mDescription = cursor.getString(index++);
channel.mVideoFormat = Utils.intern(cursor.getString(index++));
@@ -114,17 +130,29 @@ public final class Channel {
channel.mAppLinkIconUri = cursor.getString(index++);
channel.mAppLinkPosterArtUri = cursor.getString(index++);
channel.mAppLinkIntentUri = cursor.getString(index++);
+ if (Utils.isBundledInput(channel.mInputId)) {
+ channel.mRecordingProhibited = cursor.getInt(index++) != 0;
+ }
return channel;
}
/**
- * Creates a {@link Channel} object from the DVR database.
+ * Replaces the channel number separator with dash('-').
*/
- public static Channel fromDvrCursor(Cursor c) {
- Channel channel = new Channel();
- int index = -1;
- channel.mDvrId = c.getLong(++index);
- return channel;
+ public static String normalizeDisplayNumber(String string) {
+ if (!TextUtils.isEmpty(string)) {
+ int length = string.length();
+ for (int i = 0; i < length; i++) {
+ char c = string.charAt(i);
+ if (c == '.' || Character.isWhitespace(c)
+ || Character.getType(c) == Character.DASH_PUNCTUATION) {
+ StringBuilder sb = new StringBuilder(string);
+ sb.setCharAt(i, CHANNEL_NUMBER_DELIMITER);
+ return sb.toString();
+ }
+ }
+ }
+ return string;
}
/** ID of this channel. Matches to BaseColumns._ID. */
@@ -147,8 +175,10 @@ public final class Channel {
private String mAppLinkIntentUri;
private Intent mAppLinkIntent;
private int mAppLinkType;
+ private String mLogoUri;
+ private boolean mRecordingProhibited;
- private long mDvrId;
+ private boolean mChannelLogoExist;
private Channel() {
// Do nothing.
@@ -230,10 +260,14 @@ public final class Channel {
}
/**
- * Returns an ID in DVR database.
+ * Returns channel logo uri which is got from cloud, it's used only for ChannelLogoFetcher.
*/
- public long getDvrId() {
- return mDvrId;
+ public String getLogoUri() {
+ return mLogoUri;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mRecordingProhibited;
}
/**
@@ -279,6 +313,13 @@ public final class Channel {
}
/**
+ * Sets channel logo uri which is got from cloud.
+ */
+ public void setLogoUri(String logoUri) {
+ mLogoUri = logoUri;
+ }
+
+ /**
* Check whether {@code other} has same read-only channel info as this. But, it cannot check two
* channels have same logos. It also excludes browsable and locked, because two fields are
* changed by TV app.
@@ -298,7 +339,8 @@ public final class Channel {
&& mAppLinkColor == other.mAppLinkColor
&& Objects.equals(mAppLinkIconUri, other.mAppLinkIconUri)
&& Objects.equals(mAppLinkPosterArtUri, other.mAppLinkPosterArtUri)
- && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri);
+ && Objects.equals(mAppLinkIntentUri, other.mAppLinkIntentUri)
+ && Objects.equals(mRecordingProhibited, other.mRecordingProhibited);
}
@Override
@@ -315,7 +357,8 @@ public final class Channel {
+ ", isPassthrough=" + mIsPassthrough
+ ", browsable=" + mBrowsable
+ ", locked=" + mLocked
- + ", appLinkText=" + mAppLinkText + "}";
+ + ", appLinkText=" + mAppLinkText
+ + ", recordingProhibited=" + mRecordingProhibited + "}";
}
void copyFrom(Channel other) {
@@ -340,6 +383,8 @@ public final class Channel {
mAppLinkIntentUri = other.mAppLinkIntentUri;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
+ mRecordingProhibited = other.mRecordingProhibited;
+ mChannelLogoExist = other.mChannelLogoExist;
}
/**
@@ -389,8 +434,6 @@ public final class Channel {
mChannel.mDisplayName = "name";
mChannel.mDescription = "description";
mChannel.mBrowsable = true;
- mChannel.mLocked = false;
- mChannel.mIsPassthrough = false;
}
public Builder(Channel other) {
@@ -422,7 +465,7 @@ public final class Channel {
@VisibleForTesting
public Builder setDisplayNumber(String displayNumber) {
- mChannel.mDisplayNumber = displayNumber;
+ mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
return this;
}
@@ -485,6 +528,11 @@ public final class Channel {
return this;
}
+ public Builder setRecordingProhibited(boolean recordingProhibited) {
+ mChannel.mRecordingProhibited = recordingProhibited;
+ return this;
+ }
+
public Channel build() {
Channel channel = new Channel();
channel.copyFrom(mChannel);
@@ -524,6 +572,21 @@ public final class Channel {
}
/**
+ * Sets if the channel logo exists. This method should be only called from
+ * {@link ChannelDataManager}.
+ */
+ void setChannelLogoExist(boolean exist) {
+ mChannelLogoExist = exist;
+ }
+
+ /**
+ * Returns if channel logo exists.
+ */
+ public boolean channelLogoExists() {
+ return mChannelLogoExist;
+ }
+
+ /**
* Returns the type of app link for this channel.
* It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and
* a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which
@@ -655,4 +718,4 @@ public final class Channel {
return label;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java
index 6f9ea6d7..eb3871fc 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -21,10 +21,14 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
+import android.content.res.AssetFileDescriptor;
import android.database.ContentObserver;
+import android.database.sqlite.SQLiteException;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -43,6 +47,8 @@ import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -192,7 +198,6 @@ public class ChannelDataManager {
mStarted = false;
mDbLoadFinished = false;
- ChannelLogoFetcher.stopFetchingChannelLogos();
mInputManager.removeCallback(mTvInputCallback);
mContentResolver.unregisterContentObserver(mChannelObserver);
mHandler.removeCallbacksAndMessages(null);
@@ -590,6 +595,36 @@ public class ChannelDataManager {
}
}
+ private class checkChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> {
+ private final Channel mChannel;
+
+ public checkChannelLogoExistTask(Channel channel) {
+ mChannel = channel;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ boolean result = false;
+ try {
+ AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor(
+ TvContract.buildChannelLogoUri(mChannel.getId()), "r");
+ result = true;
+ f.close();
+ } catch (SQLiteException | IOException | NullPointerException e) {
+ // File not found or asset file not found.
+ }
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ ChannelWrapper wrapper = mChannelWrapperMap.get(mChannel.getId());
+ if (wrapper != null) {
+ wrapper.mChannel.setChannelLogoExist(result);
+ }
+ }
+ }
+
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
public QueryAllChannelsTask(ContentResolver contentResolver) {
@@ -625,6 +660,8 @@ public class ChannelDataManager {
boolean newlyAdded = !removedChannelIds.remove(channelId);
ChannelWrapper channelWrapper;
if (newlyAdded) {
+ new checkChannelLogoExistTask(channel)
+ .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
channelWrapper = new ChannelWrapper(channel);
mChannelWrapperMap.put(channel.getId(), channelWrapper);
if (!channelWrapper.mInputRemoved) {
@@ -640,9 +677,9 @@ public class ChannelDataManager {
// {@link #applyUpdatedValuesToDb} is called. Therefore, the value
// between DB and ChannelDataManager could be different for a while.
// Therefore, we'll keep the values in ChannelDataManager.
- channelWrapper.mChannel.copyFrom(channel);
channel.setBrowsable(oldChannel.isBrowsable());
channel.setLocked(oldChannel.isLocked());
+ channelWrapper.mChannel.copyFrom(channel);
if (!channelWrapper.mInputRemoved) {
channelUpdated = true;
updatedChannelWrappers.add(channelWrapper);
@@ -693,7 +730,6 @@ public class ChannelDataManager {
r.run();
}
mPostRunnablesAfterChannelUpdate.clear();
- ChannelLogoFetcher.startFetchingChannelLogos(mContext);
}
}
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 5a549f83..256ecdb2 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -16,155 +16,68 @@
package com.android.tv.data;
+import android.content.ContentProviderOperation;
import android.content.Context;
-import android.database.Cursor;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
import android.graphics.Bitmap.CompressFormat;
import android.media.tv.TvContract;
-import android.media.tv.TvContract.Channels;
import android.net.Uri;
import android.os.AsyncTask;
-import android.support.annotation.WorkerThread;
+import android.os.RemoteException;
+import android.support.annotation.AnyThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.util.AsyncDbTask;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import com.android.tv.util.PermissionUtils;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.Set;
+import java.util.List;
/**
- * Utility class for TMS data.
- * This class is thread safe.
+ * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
+ * or need update logos. This class is thread safe.
*/
public class ChannelLogoFetcher {
private static final String TAG = "ChannelLogoFetcher";
private static final boolean DEBUG = false;
- /**
- * The name of the file which contains the TMS data.
- * The file has multiple records and each of them is a string separated by '|' like
- * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI.
- */
- private static final String TMS_US_TABLE_FILE = "tms_us.table";
- private static final String TMS_KR_TABLE_FILE = "tms_kr.table";
- private static final String FIELD_SEPARATOR = "\\|";
- private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]";
- private static final String NAME_SEPARATOR_FOR_DB = "\\W";
- private static final int INDEX_NAME = 0;
- private static final int INDEX_SHORT_NAME = 1;
- private static final int INDEX_CALL_SIGN = 2;
- private static final int INDEX_LOGO_URI = 3;
-
- private static final String COLUMN_CHANNEL_LOGO = "logo";
+ private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
+ "is_first_time_fetch_channel_logo";
- private static final Object sLock = new Object();
- private static final Set<Long> sChannelIdBlackListSet = new HashSet<>();
- private static LoadChannelTask sQueryTask;
private static FetchLogoTask sFetchTask;
/**
- * Fetch the channel logos from TMS data and insert them into TvProvider.
+ * Fetches the channel logos from the cloud data and insert them into TvProvider.
* The previous task is canceled and a new task starts.
*/
- public static void startFetchingChannelLogos(Context context) {
+ @AnyThread
+ public static synchronized void startFetchingChannelLogos(
+ Context context, List<Channel> channels) {
if (!PermissionUtils.hasAccessAllEpg(context)) {
// TODO: support this feature for non-system LC app. b/23939816
return;
}
- synchronized (sLock) {
- stopFetchingChannelLogos();
- if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
- sQueryTask = new LoadChannelTask(context);
- sQueryTask.executeOnDbThread();
+ if (sFetchTask != null) {
+ sFetchTask.cancel(true);
}
- }
-
- /**
- * Stops the current fetching tasks. This can be called when the Activity pauses.
- */
- public static void stopFetchingChannelLogos() {
- synchronized (sLock) {
- if (DEBUG) Log.d(TAG, "Request to stop fetching logos.");
- if (sQueryTask != null) {
- sQueryTask.cancel(true);
- sQueryTask = null;
- }
- if (sFetchTask != null) {
- sFetchTask.cancel(true);
- sFetchTask = null;
- }
+ if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
+ if (channels == null || channels.isEmpty()) {
+ return;
}
+ sFetchTask = new FetchLogoTask(context, channels);
+ sFetchTask.execute();
}
private ChannelLogoFetcher() {
}
- private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> {
- private final Context mContext;
-
- public LoadChannelTask(Context context) {
- mContext = context;
- }
-
- @Override
- protected List<Channel> doInBackground(Void... arg) {
- // Load channels which doesn't have channel logos.
- if (DEBUG) Log.d(TAG, "Starts loading the channels from DB");
- String[] projection =
- new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME };
- String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND "
- + Channels.COLUMN_PACKAGE_NAME + "=?";
- String[] selectionArgs = new String[] { mContext.getPackageName() };
- try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI,
- projection, selection, selectionArgs, null)) {
- if (c == null) {
- Log.e(TAG, "Query returns null cursor", new RuntimeException());
- return null;
- }
- List<Channel> channels = new ArrayList<>();
- while (!isCancelled() && c.moveToNext()) {
- long channelId = c.getLong(0);
- if (sChannelIdBlackListSet.contains(channelId)) {
- continue;
- }
- channels.add(new Channel.Builder().setId(c.getLong(0))
- .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault()))
- .build());
- }
- return channels;
- }
- }
-
- @Override
- protected void onPostExecute(List<Channel> channels) {
- synchronized (sLock) {
- if (DEBUG) {
- int count = channels == null ? 0 : channels.size();
- Log.d(TAG, count + " channels are loaded");
- }
- if (sQueryTask == this) {
- sQueryTask = null;
- if (channels != null && !channels.isEmpty()) {
- sFetchTask = new FetchLogoTask(mContext, channels);
- sFetchTask.execute();
- }
- }
- }
- }
- }
-
private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final List<Channel> mChannels;
@@ -180,83 +93,53 @@ public class ChannelLogoFetcher {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Load the TMS table data.
- if (DEBUG) Log.d(TAG, "Loads TMS data");
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- try {
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE));
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+ List<Channel> channelsToUpdate = new ArrayList<>();
+ List<Channel> channelsToRemove = new ArrayList<>();
+ // Updates or removes the logo by comparing the logo uri which is got from the cloud
+ // and the stored one. And we assume that the data got form the cloud is 100%
+ // correct and completed.
+ SharedPreferences sharedPreferences =
+ mContext.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
+ Context.MODE_PRIVATE);
+ SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
+ Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
+ boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean(
+ PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
+ // Iterating channels.
+ for (Channel channel : mChannels) {
+ String channelIdString = Long.toString(channel.getId());
+ String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
+ if (!TextUtils.isEmpty(channel.getLogoUri())
+ && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
+ channelsToUpdate.add(channel);
+ sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
+ } else if (TextUtils.isEmpty(channel.getLogoUri())
+ && (!TextUtils.isEmpty(storedChannelLogoUri)
+ || isFirstTimeFetchChannelLogo)) {
+ channelsToRemove.add(channel);
+ sharedPreferencesEditor.remove(channelIdString);
}
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE));
- } catch (IOException e) {
- Log.e(TAG, "Loading TMS data failed.", e);
- return null;
}
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+
+ // Removes non existing channels from SharedPreferences.
+ for (String channelId : uncheckedChannels.keySet()) {
+ sharedPreferencesEditor.remove(channelId);
}
- // Iterating channels.
- for (Channel channel : mChannels) {
+ // Updates channel logos.
+ for (Channel channel : channelsToUpdate) {
if (isCancelled()) {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Download the channel logo.
- if (TextUtils.isEmpty(channel.getDisplayName())) {
- if (DEBUG) {
- Log.d(TAG, "The channel with ID (" + channel.getId()
- + ") doesn't have the display name.");
- }
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
- String channelName = channel.getDisplayName().trim();
- String logoUri = channelNameLogoUriMap.get(channelName);
- if (TextUtils.isEmpty(logoUri)) {
- if (DEBUG) {
- Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'");
- }
- // Find the candidate names. If the channel name is CNN-HD, then find CNNHD
- // and CNN. Or if the channel name is KQED+, then find KQED.
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB);
- if (splitNames.length > 1) {
- StringBuilder sb = new StringBuilder();
- for (String splitName : splitNames) {
- sb.append(splitName);
- }
- logoUri = channelNameLogoUriMap.get(sb.toString());
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
- + "'");
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)
- && splitNames[0].length() != channelName.length()) {
- logoUri = channelNameLogoUriMap.get(splitNames[0]);
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
- + "'");
- }
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)) {
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
+ // Downloads the channel logo.
+ String logoUri = channel.getLogoUri();
ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
if (bitmapInfo == null) {
Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
+ ", " + "logoUri=" + logoUri + "}");
- sChannelIdBlackListSet.add(channel.getId());
continue;
}
if (isCancelled()) {
@@ -264,12 +147,15 @@ public class ChannelLogoFetcher {
return null;
}
- // Insert the logo to DB.
+ // Inserts the logo to DB.
Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
} catch (IOException e) {
Log.e(TAG, "Failed to write " + logoUri + " to " + dstLogoUri, e);
+ // Removes it from the shared preference for the failed channels to make it
+ // retry next time.
+ sharedPreferencesEditor.remove(Long.toString(channel.getId()));
continue;
}
if (DEBUG) {
@@ -277,63 +163,30 @@ public class ChannelLogoFetcher {
+ dstLogoUri + "}");
}
}
- if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
- return null;
- }
- @WorkerThread
- private Map<String, String> readTmsFile(Context context, String fileName)
- throws IOException {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(
- context.getAssets().open(fileName)))) {
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- String line;
- while ((line = reader.readLine()) != null && !isCancelled()) {
- String[] data = line.split(FIELD_SEPARATOR);
- if (data.length != INDEX_LOGO_URI + 1) {
- if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line);
- continue;
- }
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
+ // Removes the logos for the channels that have logos before but now
+ // their logo uris are null.
+ boolean deleteChannelLogoFailed = false;
+ if (!channelsToRemove.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (Channel channel : channelsToRemove) {
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildChannelLogoUri(channel.getId())).build());
+ }
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ deleteChannelLogoFailed = true;
+ Log.e(TAG, "Error deleting obsolete channels", e);
}
- return channelNameLogoUriMap;
}
- }
-
- private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName,
- String logoUri) {
- if (!TextUtils.isEmpty(channelName)) {
- channelNameLogoUriMap.put(channelName, logoUri);
- // Find the candidate names.
- // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and
- // "W05AA-D"
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS);
- if (splitNames.length > 1) {
- for (String name : splitNames) {
- name = name.trim();
- if (channelNameLogoUriMap.get(name) == null) {
- channelNameLogoUriMap.put(name, logoUri);
- }
- }
- }
- }
- }
-
- @Override
- protected void onPostExecute(Void result) {
- synchronized (sLock) {
- if (sFetchTask == this) {
- sFetchTask = null;
- }
+ if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) {
+ sharedPreferencesEditor.putBoolean(
+ PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false);
}
+ sharedPreferencesEditor.commit();
+ if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
+ return null;
}
}
}
diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java
index 59021609..29054aa5 100644
--- a/src/com/android/tv/data/ChannelNumber.java
+++ b/src/com/android/tv/data/ChannelNumber.java
@@ -17,37 +17,38 @@
package com.android.tv.data;
import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.view.KeyEvent;
+import com.android.tv.util.StringUtils;
+
+import java.util.Objects;
+
/**
* A convenience class to handle channel number.
*/
public final class ChannelNumber implements Comparable<ChannelNumber> {
- public static final String PRIMARY_CHANNEL_DELIMITER = "-";
- public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "};
-
private static final int[] CHANNEL_DELIMITER_KEYCODES = {
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE
};
+ /** The major part of the channel number. */
public String majorNumber;
+ /** The flag which indicates whether it has a delimiter or not. */
public boolean hasDelimiter;
+ /** The major part of the channel number. */
public String minorNumber;
public ChannelNumber() {
reset();
}
- public ChannelNumber(String major, boolean hasDelimiter, String minor) {
- setChannelNumber(major, hasDelimiter, minor);
- }
-
public void reset() {
setChannelNumber("", false, "");
}
- public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
+ private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
this.majorNumber = majorNumber;
this.hasDelimiter = hasDelimiter;
this.minorNumber = minorNumber;
@@ -56,7 +57,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
@Override
public String toString() {
if (hasDelimiter) {
- return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber;
+ return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber;
}
return majorNumber;
}
@@ -75,6 +76,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return major - opponentMajor;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ChannelNumber) {
+ ChannelNumber channelNumber = (ChannelNumber) obj;
+ return TextUtils.equals(majorNumber, channelNumber.majorNumber)
+ && TextUtils.equals(minorNumber, channelNumber.minorNumber)
+ && hasDelimiter == channelNumber.hasDelimiter;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(majorNumber, hasDelimiter, minorNumber);
+ }
+
public static boolean isChannelNumberDelimiterKey(int keyCode) {
for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) {
if (delimiterKeyCode == keyCode) {
@@ -84,22 +101,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return false;
}
+ /**
+ * Returns the ChannelNumber instance.
+ * <p>
+ * Note that all the channel number argument should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static ChannelNumber parseChannelNumber(String number) {
if (number == null) {
return null;
}
ChannelNumber ret = new ChannelNumber();
- int indexOfDelimiter = -1;
- for (String delimiter : CHANNEL_DELIMITERS) {
- indexOfDelimiter = number.indexOf(delimiter);
- if (indexOfDelimiter >= 0) {
- break;
- }
- }
+ int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER);
if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) {
return null;
- }
- if (indexOfDelimiter < 0) {
+ } else if (indexOfDelimiter < 0) {
ret.majorNumber = number;
if (!isInteger(ret.majorNumber)) {
return null;
@@ -115,25 +132,31 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return ret;
}
+ /**
+ * Compares the channel numbers.
+ * <p>
+ * Note that all the channel number arguments should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static int compare(String lhs, String rhs) {
ChannelNumber lhsNumber = parseChannelNumber(lhs);
ChannelNumber rhsNumber = parseChannelNumber(rhs);
+ // Null first
if (lhsNumber == null && rhsNumber == null) {
- return 0;
+ return StringUtils.compare(lhs, rhs);
} else if (lhsNumber == null /* && rhsNumber != null */) {
return -1;
- } else if (lhsNumber != null && rhsNumber == null) {
+ } else if (rhsNumber == null) {
return 1;
}
return lhsNumber.compareTo(rhsNumber);
}
- public static boolean isInteger(String string) {
+ private static boolean isInteger(String string) {
try {
Integer.parseInt(string);
- } catch(NumberFormatException e) {
- return false;
- } catch(NullPointerException e) {
+ } catch(NumberFormatException | NullPointerException e) {
return false;
}
return true;
diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java
index 6054f089..e33ca18f 100644
--- a/src/com/android/tv/data/InternalDataUtils.java
+++ b/src/com/android/tv/data/InternalDataUtils.java
@@ -21,7 +21,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.data.Program.CriticScore;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java
index fe461f14..709863cf 100644
--- a/src/com/android/tv/data/StreamInfo.java
+++ b/src/com/android/tv/data/StreamInfo.java
@@ -38,5 +38,9 @@ public interface StreamInfo {
int getAudioChannelCount();
boolean hasClosedCaption();
boolean isVideoAvailable();
+ /**
+ * Returns true, if video or audio is available.
+ */
+ boolean isVideoOrAudioAvailable();
int getVideoUnavailableReason();
}
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 3b093b6a..ddd68ad7 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -16,13 +16,11 @@
package com.android.tv.data.epg;
-import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
-import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.Address;
import android.media.tv.TvContentRating;
@@ -46,9 +44,11 @@ import com.android.tv.TvApplication;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.InternalDataUtils;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
+import com.android.tv.tuner.util.PostalCodeUtils;
import com.android.tv.util.LocationUtils;
import com.android.tv.util.RecurringRunner;
import com.android.tv.util.Utils;
@@ -56,8 +56,10 @@ import com.android.tv.util.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@@ -69,14 +71,27 @@ public class EpgFetcher {
private static final boolean DEBUG = false;
private static final int MSG_FETCH_EPG = 1;
+ private static final int MSG_FAST_FETCH_EPG = 2;
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.SECONDS.toMillis(10);
private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1);
+ private static final long NO_INFO_FETCHED_WAIT_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
+ private static final long PROGRAM_FETCH_SHORT_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
+ private static final long PROGRAM_FETCH_LONG_DURATION_SEC = TimeUnit.DAYS.toSeconds(2)
+ + EPG_PREFETCH_RECURRING_PERIOD_MS / 1000;
+
+ // This equals log2(EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1),
+ // since we will double waiting time every other trial, therefore this limit the maximum
+ // waiting time less than half of EPG_PREFETCH_RECURRING_PERIOD_MS.
+ private static final int NO_INFO_RETRY_LIMIT = 31 - Integer.numberOfLeadingZeros(
+ (int) (EPG_PREFETCH_RECURRING_PERIOD_MS / NO_INFO_FETCHED_WAIT_MS + 1));
+
private static final int BATCH_OPERATION_COUNT = 100;
+ private static final int QUERY_CHANNEL_COUNT = 50;
private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
private static final String CONTENT_RATING_SEPARATOR = ",";
@@ -96,8 +111,11 @@ public class EpgFetcher {
private EpgFetcherHandler mHandler;
private RecurringRunner mRecurringRunner;
private boolean mStarted;
+ private boolean mScanningChannels;
+ private int mFetchRetryCount;
private long mLastEpgTimestamp = -1;
+ // @GuardedBy("this")
private String mLineupId;
public static synchronized EpgFetcher getInstance(Context context) {
@@ -122,21 +140,33 @@ public class EpgFetcher {
@Override
public void onLoadFinished() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
@Override
public void onChannelListUpdated() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
@Override
public void onChannelBrowsableChanged() {
if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()");
- handleChannelChanged();
+ if (!mScanningChannels) {
+ handleChannelChanged();
+ }
}
});
+ // Warm up to get address, because the first call of getCurrentAddress is usually failed.
+ try {
+ LocationUtils.getCurrentAddress(mContext);
+ } catch (SecurityException | IOException e) {
+ // Do nothing
+ }
}
private void handleChannelChanged() {
@@ -145,7 +175,9 @@ public class EpgFetcher {
stop();
}
} else {
- start();
+ if (canStart()) {
+ start();
+ }
}
}
@@ -173,17 +205,14 @@ public class EpgFetcher {
if (!TextUtils.isEmpty(getLastLineupId())) {
return true;
}
- if (mContext.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
- != PackageManager.PERMISSION_GRANTED) {
- if (DEBUG) Log.d(TAG, "No permission to check the current location.");
- return false;
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
}
-
try {
Address address = LocationUtils.getCurrentAddress(mContext);
if (address != null
&& !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
- if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode());
+ Log.i(TAG, "Country not supported: " + address.getCountryCode());
return false;
}
} catch (SecurityException e) {
@@ -197,9 +226,13 @@ public class EpgFetcher {
/**
* Starts fetching EPG.
+ *
+ * @param resetNextRunTime if true, next run time is reset, so EPG will be fetched
+ * {@link #EPG_PREFETCH_RECURRING_PERIOD_MS} later.
+ * otherwise, EPG is fetched when this method is called.
*/
@MainThread
- public void start() {
+ private void startInternal(boolean resetNextRunTime) {
if (DEBUG) Log.d(TAG, "start()");
if (mStarted) {
if (DEBUG) Log.d(TAG, "EpgFetcher thread already started.");
@@ -215,19 +248,35 @@ public class EpgFetcher {
mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this);
mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
new EpgRunner(), null);
- mRecurringRunner.start();
+ mRecurringRunner.start(resetNextRunTime);
if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully.");
}
+ @MainThread
+ public void start() {
+ if (System.currentTimeMillis() - getLastUpdatedEpgTimestamp() >
+ EPG_PREFETCH_RECURRING_PERIOD_MS) {
+ startImmediately(false);
+ } else {
+ startInternal(false);
+ }
+ }
+
/**
* Starts fetching EPG immediately if possible without waiting for the timer.
+ *
+ * @param clearStoredLineupId if true, stored lineup id will be clear before fetching EPG.
*/
@MainThread
- public void startImmediately() {
- start();
+ public void startImmediately(boolean clearStoredLineupId) {
+ startInternal(true);
if (mStarted) {
+ if (clearStoredLineupId) {
+ if (DEBUG) Log.d(TAG, "Clear stored lineup id: " + mLineupId);
+ setLastLineupId(null);
+ }
if (DEBUG) Log.d(TAG, "Starting fetcher immediately");
- fetchEpg();
+ postFetchRequest(true, 0);
}
}
@@ -246,48 +295,71 @@ public class EpgFetcher {
mHandler.getLooper().quit();
}
- private void fetchEpg() {
- fetchEpg(0);
+ /**
+ * Notifies EPG fetcher that channel scanning is started.
+ */
+ @MainThread
+ public void onChannelScanStarted() {
+ stop();
+ mScanningChannels = true;
}
- private void fetchEpg(long delay) {
- mHandler.removeMessages(MSG_FETCH_EPG);
- mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay);
+ /**
+ * Notifies EPG fetcher that channel scanning is finished.
+ */
+ @MainThread
+ public void onChannelScanFinished() {
+ mScanningChannels = false;
+ start();
+ }
+
+ private void postFetchRequest(boolean fastFetch, long delay) {
+ int msg = fastFetch ? MSG_FAST_FETCH_EPG : MSG_FETCH_EPG;
+ mHandler.removeMessages(msg);
+ mHandler.sendEmptyMessageDelayed(msg, delay);
}
private void onFetchEpg() {
+ onFetchEpg(false);
+ }
+
+ private void onFetchEpg(boolean fastFetch) {
if (DEBUG) Log.d(TAG, "Start fetching EPG.");
if (!mEpgReader.isAvailable()) {
- if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
- fetchEpg(EPG_READER_INIT_WAIT_MS);
+ Log.i(TAG, "EPG reader is not temporarily available.");
+ postFetchRequest(fastFetch, EPG_READER_INIT_WAIT_MS);
return;
}
String lineupId = getLastLineupId();
if (lineupId == null) {
- Address address;
try {
- address = LocationUtils.getCurrentAddress(mContext);
+ PostalCodeUtils.updatePostalCode(mContext);
} catch (IOException e) {
- if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
- fetchEpg(LOCATION_ERROR_WAIT_MS);
- return;
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
+ postFetchRequest(fastFetch, LOCATION_ERROR_WAIT_MS);
+ return;
+ }
} catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location.");
- return;
- }
- if (address == null) {
- if (DEBUG) Log.d(TAG, "Null address returned.");
- fetchEpg(LOCATION_INIT_WAIT_MS);
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ Log.w(TAG, "No permission to get the current location.");
+ return;
+ }
+ } catch (PostalCodeUtils.NoPostalCodeException e) {
+ Log.i(TAG, "Failed to get the current postal code.");
+ postFetchRequest(fastFetch, LOCATION_INIT_WAIT_MS);
return;
}
- if (DEBUG) Log.d(TAG, "Current location is " + address);
+ String postalCode = PostalCodeUtils.getLastPostalCode(mContext);
+ if (DEBUG) Log.d(TAG, "The current postal code is " + postalCode);
- lineupId = getLineupForAddress(address);
+ lineupId = pickLineupForPostalCode(postalCode);
if (lineupId != null) {
- if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address);
+ Log.i(TAG, "Selecting the lineup " + lineupId);
setLastLineupId(lineupId);
} else {
- if (DEBUG) Log.d(TAG, "No lineup found for " + address);
+ Log.i(TAG, "Failed to get lineup id");
+ retryFetchEpg(fastFetch);
return;
}
}
@@ -299,48 +371,109 @@ public class EpgFetcher {
return;
}
- boolean updated = false;
List<Channel> channels = mEpgReader.getChannels(lineupId);
- for (Channel channel : channels) {
- List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId()));
- Collections.sort(programs);
- if (DEBUG) {
- Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel);
- }
- if (updateEpg(channel.getId(), programs)) {
- updated = true;
+ if (channels.isEmpty()) {
+ Log.i(TAG, "Failed to get EPG channels.");
+ retryFetchEpg(fastFetch);
+ return;
+ }
+ mFetchRetryCount = 0;
+ if (!fastFetch) {
+ for (Channel channel : channels) {
+ if (!mStarted) {
+ break;
+ }
+ List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId()));
+ Collections.sort(programs);
+ Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channel);
+ updateEpg(channel.getId(), programs);
}
+ setLastUpdatedEpgTimestamp(epgTimestamp);
+ } else {
+ handleFastFetch(channels, PROGRAM_FETCH_SHORT_DURATION_SEC);
+ if (DEBUG) Log.d(TAG, "First fast fetch Done.");
+ handleFastFetch(channels, PROGRAM_FETCH_LONG_DURATION_SEC);
+ if (DEBUG) Log.d(TAG, "Second fast fetch Done.");
}
- final boolean epgUpdated = updated;
- setLastUpdatedEpgTimestamp(epgTimestamp);
- mHandler.removeMessages(MSG_FETCH_EPG);
+ if (!fastFetch) {
+ mHandler.removeMessages(MSG_FETCH_EPG);
+ }
if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ // Start to fetch channel logos after epg fetching finished.
+ ChannelLogoFetcher.startFetchingChannelLogos(mContext, channels);
}
- @Nullable
- private String getLineupForAddress(Address address) {
- String lineup = null;
- if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
- String postalCode = address.getPostalCode();
- if (!TextUtils.isEmpty(postalCode)) {
- lineup = getLineupForPostalCode(postalCode);
+ private void retryFetchEpg(boolean fastFetch) {
+ if (mFetchRetryCount < NO_INFO_RETRY_LIMIT) {
+ postFetchRequest(fastFetch, NO_INFO_FETCHED_WAIT_MS * 1 << mFetchRetryCount);
+ mFetchRetryCount++;
+ } else {
+ mFetchRetryCount = 0;
+ }
+ }
+
+ private void handleFastFetch(List<Channel> channels, long duration) {
+ List<Long> channelIds = new ArrayList<>(channels.size());
+ for (Channel channel : channels) {
+ channelIds.add(channel.getId());
+ }
+ Map<Long, List<Program>> allPrograms = new HashMap<>();
+ List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
+ for (Long channelId : channelIds) {
+ queryChannelIds.add(channelId);
+ if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
+ allPrograms.putAll(
+ new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration)));
+ queryChannelIds.clear();
}
}
- return lineup;
+ if (!queryChannelIds.isEmpty()) {
+ allPrograms.putAll(
+ new HashMap<>(mEpgReader.getPrograms(queryChannelIds, duration)));
+ }
+ for (Channel channel : channels) {
+ List<Program> programs = allPrograms.get(channel.getId());
+ if (programs == null) continue;
+ Collections.sort(programs);
+ Log.i(TAG, "Fast fetched " + programs.size() + " programs for channel " + channel);
+ updateEpg(channel.getId(), programs);
+ }
}
@Nullable
- private String getLineupForPostalCode(String postalCode) {
+ private String pickLineupForPostalCode(String postalCode) {
List<Lineup> lineups = mEpgReader.getLineups(postalCode);
+ int maxCount = 0;
+ String maxLineupId = null;
for (Lineup lineup : lineups) {
- // TODO(EPG): handle more than OTA digital
- if (lineup.type == Lineup.LINEUP_BROADCAST_DIGITAL) {
- if (DEBUG) Log.d(TAG, "Setting lineup to " + lineup.name + "(" + lineup.id + ")");
- return lineup.id;
+ int count = getMatchedChannelCount(lineup.id);
+ Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches");
+ if (count > maxCount) {
+ maxCount = count;
+ maxLineupId = lineup.id;
}
}
- return null;
+ return maxLineupId;
+ }
+
+ private int getMatchedChannelCount(String lineupId) {
+ // Construct a list of display numbers for existing channels.
+ List<Channel> channels = mChannelDataManager.getChannelList();
+ if (channels.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "No existing channel to compare");
+ return 0;
+ }
+ List<String> numbers = new ArrayList<>(channels.size());
+ for (Channel c : channels) {
+ // We only support local channels from physical tuners.
+ if (c.isPhysicalTunerChannel()) {
+ numbers.add(c.getDisplayNumber());
+ }
+ }
+
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
}
private long getLastUpdatedEpgTimestamp() {
@@ -357,16 +490,16 @@ public class EpgFetcher {
KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit();
}
- private String getLastLineupId() {
+ synchronized private String getLastLineupId() {
if (mLineupId == null) {
mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext)
.getString(KEY_LAST_LINEUP_ID, null);
}
- if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId);
+ if (DEBUG) Log.d(TAG, "Last lineup is " + mLineupId);
return mLineupId;
}
- private void setLastLineupId(String lineupId) {
+ synchronized private void setLastLineupId(String lineupId) {
mLineupId = lineupId;
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
.putString(KEY_LAST_LINEUP_ID, lineupId).commit();
@@ -381,19 +514,9 @@ public class EpgFetcher {
long startTimeMs = System.currentTimeMillis();
long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION;
List<Program> oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs);
- Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null;
int oldProgramsIndex = 0;
int newProgramsIndex = 0;
- // Skip the past programs. They will be automatically removed by the system.
- if (currentOldProgram != null) {
- long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis();
- for (Program program : newPrograms) {
- if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) {
- break;
- }
- newProgramsIndex++;
- }
- }
+
// Compare the new programs with old programs one by one and update/delete the old one
// or insert new program if there is no matching program in the database.
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
@@ -439,7 +562,7 @@ public class EpgFetcher {
}
if (addNewProgram) {
ops.add(ContentProviderOperation
- .newInsert(TvContract.Programs.CONTENT_URI)
+ .newInsert(Programs.CONTENT_URI)
.withValues(toContentValues(newProgram))
.build());
}
@@ -501,27 +624,25 @@ public class EpgFetcher {
@SuppressWarnings("deprecation")
private static ContentValues toContentValues(Program program) {
ContentValues values = new ContentValues();
- values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId());
- putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
+ values.put(Programs.COLUMN_CHANNEL_ID, program.getChannelId());
+ putValue(values, Programs.COLUMN_TITLE, program.getTitle());
+ putValue(values, Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle());
if (BuildCompat.isAtLeastN()) {
- putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
- program.getSeasonNumber());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
- program.getEpisodeNumber());
+ putValue(values, Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber());
+ putValue(values, Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber());
} else {
- putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
- putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
+ putValue(values, Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber());
+ putValue(values, Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber());
}
- putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
- putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
- putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
+ putValue(values, Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
+ putValue(values, Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
+ putValue(values, Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
+ putValue(values, Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
String[] canonicalGenres = program.getCanonicalGenres();
if (canonicalGenres != null && canonicalGenres.length > 0) {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
- Genres.encode(canonicalGenres));
+ putValue(values, Programs.COLUMN_CANONICAL_GENRE, Genres.encode(canonicalGenres));
} else {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
+ putValue(values, Programs.COLUMN_CANONICAL_GENRE, "");
}
TvContentRating[] ratings = program.getContentRatings();
if (ratings != null && ratings.length > 0) {
@@ -530,14 +651,13 @@ public class EpgFetcher {
sb.append(CONTENT_RATING_SEPARATOR);
sb.append(ratings[i].flattenToString());
}
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString());
+ putValue(values, Programs.COLUMN_CONTENT_RATING, sb.toString());
} else {
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, "");
+ putValue(values, Programs.COLUMN_CONTENT_RATING, "");
}
- values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
- program.getStartTimeUtcMillis());
- values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
- putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA,
+ values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis());
+ values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis());
+ putValue(values, Programs.COLUMN_INTERNAL_PROVIDER_DATA,
InternalDataUtils.serializeInternalProviderData(program));
return values;
}
@@ -569,6 +689,9 @@ public class EpgFetcher {
case MSG_FETCH_EPG:
epgFetcher.onFetchEpg();
break;
+ case MSG_FAST_FETCH_EPG:
+ epgFetcher.onFetchEpg(true);
+ break;
default:
super.handleMessage(msg);
break;
@@ -579,7 +702,7 @@ public class EpgFetcher {
private class EpgRunner implements Runnable {
@Override
public void run() {
- fetchEpg();
+ postFetchRequest(false, 0);
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 4f3b6f52..95cd933e 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -22,9 +22,10 @@ import android.support.annotation.WorkerThread;
import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesInfo;
+import com.android.tv.dvr.data.SeriesInfo;
import java.util.List;
+import java.util.Map;
/**
* An interface used to retrieve the EPG data. This class should be used in worker thread.
@@ -43,21 +44,37 @@ public interface EpgReader {
long getEpgTimestamp();
/**
- * Returns the channels list.
+ * Returns the lineups list.
+ */
+ List<Lineup> getLineups(@NonNull String postalCode);
+
+ /**
+ * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to
+ * choose the most appropriate lineup among others by comparing the channel numbers of the
+ * existing channels on the device.
+ */
+ List<String> getChannelNumbers(@NonNull String lineupId);
+
+ /**
+ * Returns the list of channels for the given lineup. The returned channels should map into the
+ * existing channels on the device. This method is usually called after selecting the lineup.
*/
List<Channel> getChannels(@NonNull String lineupId);
/**
- * Returns the lineups list.
+ * Returns the programs for the given channel. Must call {@link #getChannels(String)}
+ * beforehand. Note that the {@code Program} doesn't have valid program ID because it's not
+ * retrieved from TvProvider.
*/
- List<Lineup> getLineups(@NonNull String postalCode);
+ List<Program> getPrograms(long channelId);
/**
- * Returns the programs for the given channel. The result is sorted by the start time.
+ * Returns the programs for the given channels.
* Note that the {@code Program} doesn't have valid program ID because it's not retrieved from
* TvProvider.
+ * This method is only used to get programs for a short duration typically.
*/
- List<Program> getPrograms(long channelId);
+ Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration);
/**
* Returns the series information for the given series ID.
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 64093f89..220daf22 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -21,10 +21,11 @@ import android.content.Context;
import com.android.tv.data.Channel;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
-import com.android.tv.dvr.SeriesInfo;
+import com.android.tv.dvr.data.SeriesInfo;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* A stub class to read EPG.
@@ -44,12 +45,17 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Channel> getChannels(String lineupId) {
+ public List<Lineup> getLineups(String postalCode) {
return Collections.emptyList();
}
@Override
- public List<Lineup> getLineups(String postalCode) {
+ public List<String> getChannelNumbers(String lineupId) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<Channel> getChannels(String lineupId) {
return Collections.emptyList();
}
@@ -59,6 +65,11 @@ public class StubEpgReader implements EpgReader{
}
@Override
+ public Map<Long, List<Program>> getPrograms(List<Long> channelIds, long duration) {
+ return Collections.emptyMap();
+ }
+
+ @Override
public SeriesInfo getSeriesInfo(String seriesId) {
return null;
}
diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
new file mode 100644
index 00000000..2ed98b87
--- /dev/null
+++ b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
@@ -0,0 +1,129 @@
+/*
+ * 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.dialog;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+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.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays the DVR history.
+ */
+@TargetApi(VERSION_CODES.N)
+public class DvrHistoryDialogFragment extends SafeDismissDialogFragment {
+ public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName();
+
+ private static final String TRACKER_LABEL = "DVR history";
+ private final List<ScheduledRecording> mSchedules = new ArrayList<>();
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ DvrDataManager dataManager = singletons.getDvrDataManager();
+ ChannelDataManager channelDataManager = singletons.getChannelDataManager();
+ for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) {
+ if (!schedule.isInProgress() && !schedule.isNotStarted()) {
+ mSchedules.add(schedule);
+ }
+ }
+ mSchedules.sort(ScheduledRecording.START_TIME_COMPARATOR.reversed());
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ ArrayAdapter adapter = new ArrayAdapter<ScheduledRecording>(getContext(),
+ R.layout.list_item_dvr_history, ScheduledRecording.toArray(mSchedules)) {
+ @NonNull
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false);
+ ScheduledRecording schedule = mSchedules.get(position);
+ setText(view, R.id.state, getStateString(schedule.getState()));
+ setText(view, R.id.schedule_time, getRecordingTimeText(schedule));
+ setText(view, R.id.program_title,
+ schedule.getProgramTitleWithEpisodeNumber(getContext()));
+ setText(view, R.id.channel_name, getChannelNameText(schedule));
+ return view;
+ }
+
+ private void setText(View view, int id, String text) {
+ ((TextView) view.findViewById(id)).setText(text);
+ }
+
+ private void setText(View view, int id, int text) {
+ ((TextView) view.findViewById(id)).setText(text);
+ }
+
+ @SuppressLint("SwitchIntDef")
+ private int getStateString(@RecordingState int state) {
+ switch (state) {
+ case ScheduledRecording.STATE_RECORDING_CLIPPED:
+ return R.string.dvr_history_dialog_state_clip;
+ case ScheduledRecording.STATE_RECORDING_FAILED:
+ return R.string.dvr_history_dialog_state_fail;
+ case ScheduledRecording.STATE_RECORDING_FINISHED:
+ return R.string.dvr_history_dialog_state_success;
+ default:
+ break;
+ }
+ return 0;
+ }
+
+ private String getChannelNameText(ScheduledRecording schedule) {
+ Channel channel = channelDataManager.getChannel(schedule.getChannelId());
+ return channel == null ? null :
+ TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() :
+ channel.getDisplayName().trim() + " " + channel.getDisplayNumber();
+ }
+
+ private String getRecordingTimeText(ScheduledRecording schedule) {
+ return Utils.getDurationString(getContext(), schedule.getStartTimeMs(),
+ schedule.getEndTimeMs(), true, true, true, 0);
+ }
+ };
+ ListView listView = new ListView(getActivity());
+ listView.setAdapter(adapter);
+ return new AlertDialog.Builder(getActivity()).setTitle(R.string.dvr_history_dialog_title)
+ .setView(listView).create();
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+}
diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java
index d16202a1..d00422a7 100644
--- a/src/com/android/tv/dialog/FullscreenDialogFragment.java
+++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java
@@ -77,7 +77,7 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment {
return mTrackerLabel;
}
- private class FullscreenDialog extends TvDialog {
+ private class FullscreenDialog extends Dialog {
public FullscreenDialog(Context context, int theme) {
super(context, theme);
}
diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dialog/HalfSizedDialogFragment.java
index d320816e..315c6a93 100644
--- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
+++ b/src/com/android/tv/dialog/HalfSizedDialogFragment.java
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dialog;
+import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
@@ -25,7 +26,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.dialog.SafeDismissDialogFragment;
import java.util.concurrent.TimeUnit;
@@ -54,13 +54,6 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
@Override
public void onStart() {
super.onStart();
- getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
- public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) {
- mHandler.removeCallbacks(mAutoDismisser);
- mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
- return false;
- }
- });
mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
}
@@ -81,6 +74,19 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
}
@Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) {
+ mHandler.removeCallbacks(mAutoDismisser);
+ mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
+ return false;
+ }
+ });
+ return dialog;
+ }
+
+ @Override
public int getTheme() {
return R.style.Theme_TV_dialog_HalfSizedDialog;
}
diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
index f671a87d..e3390b0a 100644
--- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java
+++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
@@ -17,11 +17,7 @@
package com.android.tv.dialog;
import android.app.Activity;
-import android.app.Dialog;
import android.app.DialogFragment;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.KeyEvent;
import com.android.tv.MainActivity;
import com.android.tv.TvApplication;
@@ -39,11 +35,6 @@ public abstract class SafeDismissDialogFragment extends DialogFragment
private Tracker mTracker;
@Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new TvDialog(getActivity(), getTheme());
- }
-
- @Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mAttached = true;
@@ -92,21 +83,4 @@ public abstract class SafeDismissDialogFragment extends DialogFragment
super.dismiss();
}
}
-
- protected class TvDialog extends Dialog {
- public TvDialog(Context context, int theme) {
- super(context, theme);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- // When a dialog is showing, key events are handled by the dialog instead of
- // MainActivity. Therefore, unless a key is a global key, it should be handled here.
- if (mAttached && keyCode == KeyEvent.KEYCODE_SEARCH && mActivity != null) {
- mActivity.showSearchActivity();
- return true;
- }
- return super.onKeyUp(keyCode, event);
- }
- }
}
diff --git a/src/com/android/tv/dialog/WebDialogFragment.java b/src/com/android/tv/dialog/WebDialogFragment.java
index 75f93bb2..171a256b 100644
--- a/src/com/android/tv/dialog/WebDialogFragment.java
+++ b/src/com/android/tv/dialog/WebDialogFragment.java
@@ -37,6 +37,7 @@ public class WebDialogFragment extends SafeDismissDialogFragment {
private static final String TITLE = "TITLE";
private static final String TRACKER_LABEL = "TRACKER_LABEL";
+ private WebView mWebView;
private String mTrackerLabel;
/**
@@ -73,13 +74,21 @@ public class WebDialogFragment extends SafeDismissDialogFragment {
String title = getArguments().getString(TITLE);
getDialog().setTitle(title);
- WebView webView = new WebView(getActivity());
- webView.setWebViewClient(new WebViewClient());
+ mWebView = new WebView(getActivity());
+ mWebView.setWebViewClient(new WebViewClient());
String url = getArguments().getString(URL);
- webView.loadUrl(url);
+ mWebView.loadUrl(url);
Log.d(TAG, "Loading web content from " + url);
- return webView;
+ return mWebView;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mWebView != null) {
+ mWebView.destroy();
+ }
}
@Override
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 89661df3..a8637449 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -26,7 +26,10 @@ import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.Clock;
import java.util.ArrayList;
@@ -318,5 +321,41 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) {
+ List<SeriesRecording> toRemove = new ArrayList<>();
+ for (long rId : seriesRecordingIds) {
+ SeriesRecording seriesRecording = getSeriesRecording(rId);
+ if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) {
+ toRemove.add(seriesRecording);
+ }
+ }
+ removeSeriesRecording(SeriesRecording.toArray(toRemove));
+ }
+
+ /**
+ * Returns {@code true}, if the series recording is empty and can be removed. If a series
+ * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be
+ * removed.
+ */
+ protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) {
+ if (!seriesRecording.isStopped()) {
+ return false;
+ }
+ long seriesRecordingId = seriesRecording.getId();
+ for (ScheduledRecording r : getAvailableScheduledRecordings()) {
+ if (r.getSeriesRecordingId() == seriesRecordingId) {
+ return false;
+ }
+ }
+ String seriesId = seriesRecording.getSeriesId();
+ for (RecordedProgram r : getRecordedPrograms()) {
+ if (seriesId.equals(r.getSeriesId())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
public void forgetStorage(String inputId) { }
}
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index 06613667..6d400b82 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -21,7 +21,10 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Range;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.Collection;
import java.util.List;
@@ -211,6 +214,13 @@ public interface DvrDataManager {
Collection<Long> getDisallowedProgramIds();
/**
+ * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains
+ * any available schedules or recorded programs, and it's status is
+ * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings.
+ */
+ void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds);
+
+ /**
* Listens for the DVR schedules loading finished.
*/
interface OnDvrScheduleLoadFinishedListener {
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 46682a48..6d0a9959 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -42,7 +42,11 @@ import android.util.Range;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.IdGenerator;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask;
@@ -51,6 +55,8 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask;
+import com.android.tv.dvr.provider.DvrDbSync;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
import com.android.tv.util.Clock;
@@ -267,11 +273,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
removeScheduledRecording(ScheduledRecording.toArray(toDelete));
}
IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
+ if (mRecordedProgramLoadFinished) {
+ validateSeriesRecordings();
+ }
mDvrLoadFinished = true;
notifyDvrScheduleLoadFinished();
- mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
- mDbSync.start();
if (isInitialized()) {
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync.start();
SeriesRecordingScheduler.getInstance(mContext).start();
}
}
@@ -306,6 +315,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (uri == null) {
uri = RecordedPrograms.CONTENT_URI;
}
+ if (recordedPrograms == null) {
+ recordedPrograms = Collections.emptyList();
+ }
int match = TvProviderUriMatcher.match(uri);
if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) {
if (!mRecordedProgramLoadFinished) {
@@ -318,7 +330,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
mRecordedProgramLoadFinished = true;
notifyRecordedProgramLoadFinished();
- } else if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ if (isInitialized()) {
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync.start();
+ }
+ } else if (recordedPrograms.isEmpty()) {
List<RecordedProgram> oldRecordedPrograms =
new ArrayList<>(mRecordedPrograms.values());
mRecordedPrograms.clear();
@@ -355,6 +371,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (isInitialized()) {
+ validateSeriesRecordings();
SeriesRecordingScheduler.getInstance(mContext).start();
}
} else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
@@ -363,11 +380,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
long id = ContentUris.parseId(uri);
if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
- if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ if (recordedPrograms.isEmpty()) {
mRecordedProgramsForRemovedInput.remove(id);
RecordedProgram old = mRecordedPrograms.remove(id);
if (old != null) {
notifyRecordedProgramsRemoved(old);
+ SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId());
+ if (r != null && isEmptySeriesRecording(r)) {
+ removeSeriesRecording(r);
+ }
}
} else {
RecordedProgram recordedProgram = recordedPrograms.get(0);
@@ -592,10 +613,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>();
+ Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
mScheduledRecordings.remove(r.getId());
- getDeletedScheduleMap().remove(r.getId());
+ getDeletedScheduleMap().remove(r.getProgramId());
mProgramId2ScheduledRecordings.remove(r.getProgramId());
+ if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
+ && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
+ }
boolean isScheduleForRemovedInput =
mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null;
// If it belongs to the series recording and it's not started yet, just mark delete
@@ -614,8 +641,19 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (mDvrLoadFinished) {
+ if (mRecordedProgramLoadFinished) {
+ checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
+ }
notifyScheduledRecordingRemoved(schedules);
}
+ Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator();
+ while (iterator.hasNext()) {
+ ScheduledRecording r = iterator.next();
+ if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) {
+ iterator.remove();
+ schedulesToDelete.add(r);
+ }
+ }
if (!schedulesToDelete.isEmpty()) {
new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
ScheduledRecording.toArray(schedulesToDelete));
@@ -669,6 +707,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) {
List<ScheduledRecording> toUpdate = new ArrayList<>();
+ Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG,
"Recording not found for: " + r)) {
@@ -691,6 +730,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (programId != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(programId, r);
}
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
+ && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
+ // If the scheduled recording is failed, it may cause the automatically generated
+ // series recording for this schedule becomes invalid (with no future schedules and
+ // past recordings.) We should check and remove these series recordings.
+ seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
+ }
}
if (toUpdate.isEmpty()) {
return;
@@ -702,12 +748,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (updateDb) {
new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
}
+ checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
removeDeletedSchedules(schedules);
}
@Override
public void updateSeriesRecording(final SeriesRecording... seriesRecordings) {
for (SeriesRecording r : seriesRecordings) {
+ if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG,
+ "Non Existing Series ID: " + r)) {
+ continue;
+ }
SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r);
SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be"
@@ -769,14 +820,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return r.getInputId().equals(inputId);
}
});
- List<SeriesRecording> movedSeriesRecordings =
- moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
List<RecordedProgram> movedRecordedPrograms =
moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms,
new Filter<RecordedProgram>() {
@@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return r.getInputId().equals(inputId);
}
});
+ List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
+ List<SeriesRecording> movedSeriesRecordings =
+ moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings,
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ if (r.getInputId().equals(inputId)) {
+ if (!isEmptySeriesRecording(r)) {
+ return true;
+ }
+ removedSeriesRecordings.add(r);
+ }
+ return false;
+ }
+ });
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule);
@@ -795,6 +853,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording);
}
}
+ for (SeriesRecording r : removedSeriesRecordings) {
+ mSeriesRecordingsForRemovedInput.remove(r.getId());
+ }
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(
+ SeriesRecording.toArray(removedSeriesRecordings));
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
@@ -811,20 +874,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (DEBUG) Log.d(TAG, "hideInput " + inputId);
List<ScheduledRecording> movedSchedules =
moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ new Filter<ScheduledRecording>() {
+ @Override
+ public boolean filter(ScheduledRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
List<SeriesRecording> movedSeriesRecordings =
moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
List<RecordedProgram> movedRecordedPrograms =
moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput,
new Filter<RecordedProgram>() {
@@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
+ private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) {
+ int i = 0;
+ long[] rIds = new long[seriesRecordingIds.size()];
+ for (long rId : seriesRecordingIds) {
+ rIds[i++] = rId;
+ }
+ checkAndRemoveEmptySeriesRecording(rIds);
+ }
+
@Override
public void forgetStorage(String inputId) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
@@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}.executeOnDbThread();
}
+ private void validateSeriesRecordings() {
+ Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator();
+ List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
+ while (iter.hasNext()) {
+ SeriesRecording r = iter.next();
+ if (isEmptySeriesRecording(r)) {
+ iter.remove();
+ removedSeriesRecordings.add(r);
+ }
+ }
+ if (!removedSeriesRecordings.isEmpty()) {
+ SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
+ if (mDvrLoadFinished) {
+ notifySeriesRecordingRemoved(removed);
+ }
+ }
+ }
+
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 5fa6f90f..2effe9cf 100644
--- a/src/com/android/tv/dvr/DvrManager.java
+++ b/src/com/android/tv/dvr/DvrManager.java
@@ -46,7 +46,9 @@ import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
-import com.android.tv.dvr.SeriesRecording.SeriesState;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Utils;
@@ -234,7 +236,7 @@ public class DvrManager {
* Adds a new series recording and schedules for the programs with the initial state.
*/
public SeriesRecording addSeriesRecording(Program selectedProgram,
- List<Program> programsToSchedule, @SeriesState int initialState) {
+ List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) {
Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: "
+ programsToSchedule);
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
@@ -308,8 +310,7 @@ public class DvrManager {
ScheduledRecording scheduleWithSameProgram =
mDataManager.getScheduledRecordingForProgramId(program.getId());
if (scheduleWithSameProgram != null) {
- if (scheduleWithSameProgram.getState()
- == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ if (scheduleWithSameProgram.isNotStarted()) {
ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram)
.setSeriesRecordingId(series.getId())
.build();
@@ -337,10 +338,10 @@ public class DvrManager {
*/
public void updateSeriesRecording(SeriesRecording series) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext);
- scheduler.pauseUpdate();
SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
if (previousSeries != null) {
+ // If the channel option of series changed, remove the existing schedules. The new
+ // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment.
if (previousSeries.getChannelOption() != series.getChannelOption()
|| (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
&& previousSeries.getChannelId() != series.getChannelId())) {
@@ -350,6 +351,18 @@ public class DvrManager {
for (ScheduledRecording schedule : schedules) {
if (schedule.isNotStarted()) {
schedulesToRemove.add(schedule);
+ } else if (schedule.isInProgress()
+ && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
+ && schedule.getChannelId() != series.getChannelId()) {
+ stopRecording(schedule);
+ }
+ }
+ List<ScheduledRecording> deletedSchedules =
+ new ArrayList<>(mDataManager.getDeletedSchedules());
+ for (ScheduledRecording deletedSchedule : deletedSchedules) {
+ if (deletedSchedule.getSeriesRecordingId() == series.getId()
+ && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) {
+ schedulesToRemove.add(deletedSchedule);
}
}
mDataManager.removeScheduledRecording(true,
@@ -363,7 +376,7 @@ public class DvrManager {
List<ScheduledRecording> schedulesToUpdate = new ArrayList<>();
for (ScheduledRecording schedule
: mDataManager.getScheduledRecordings(series.getId())) {
- if (schedule.isNotStarted()) {
+ if (schedule.isNotStarted() || schedule.isInProgress()) {
schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule)
.setPriority(priority).build());
}
@@ -373,7 +386,6 @@ public class DvrManager {
ScheduledRecording.toArray(schedulesToUpdate));
}
}
- scheduler.resumeUpdate();
}
}
@@ -400,33 +412,6 @@ public class DvrManager {
}
/**
- * Returns true, if the series recording can be removed. If a series recording is NORMAL state
- * or has recordings or schedules, it cannot be removed.
- */
- public boolean canRemoveSeriesRecording(long seriesRecordingId) {
- SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId);
- if (seriesRecording == null) {
- return false;
- }
- if (!seriesRecording.isStopped()) {
- return false;
- }
- for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
- if (r.getSeriesRecordingId() == seriesRecordingId) {
- return false;
- }
- }
- String seriesId = seriesRecording.getSeriesId();
- SoftPreconditions.checkNotNull(seriesId);
- for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
- if (seriesId.equals(r.getSeriesId())) {
- return false;
- }
- }
- return true;
- }
-
- /**
* Stops the currently recorded program
*/
public void stopRecording(final ScheduledRecording recording) {
@@ -657,6 +642,9 @@ public class DvrManager {
if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) {
return false;
}
+ if (channel.isRecordingProhibited()) {
+ return false;
+ }
TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
if (info == null) {
Log.w(TAG, "Could not find TvInputInfo for " + channel);
@@ -680,7 +668,12 @@ public class DvrManager {
if (!mDataManager.isInitialized()) {
return false;
}
- TvInputInfo info = Utils.getTvInputInfoForProgram(mAppContext, program);
+ Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager()
+ .getChannel(program.getChannelId());
+ if (channel == null || channel.isRecordingProhibited()) {
+ return false;
+ }
+ TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
if (info == null) {
Log.w(TAG, "Could not find TvInputInfo for " + program);
return false;
@@ -733,6 +726,17 @@ public class DvrManager {
return mDataManager.getSeriesRecording(program.getSeriesId());
}
+ /**
+ * Returns if there are valid items. Valid item contains {@link RecordedProgram},
+ * available {@link ScheduledRecording} and {@link SeriesRecording}.
+ */
+ public boolean hasValidItems() {
+ return !(mDataManager.getRecordedPrograms().isEmpty()
+ && mDataManager.getStartedRecordings().isEmpty()
+ && mDataManager.getNonStartedScheduledRecordings().isEmpty()
+ && mDataManager.getSeriesRecordings().isEmpty());
+ }
+
@WorkerThread
@VisibleForTesting
// Should be public to use mock DvrManager object.
@@ -840,9 +844,10 @@ public class DvrManager {
}
/**
- * Listener internally used inside dvr package.
+ * Listener to stop recording request. Should only be internally used inside dvr and its
+ * sub-package.
*/
- interface Listener {
+ public interface Listener {
void onStopRecordingRequested(ScheduledRecording scheduledRecording);
}
}
diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java
index a5851a75..b72117aa 100644
--- a/src/com/android/tv/dvr/DvrScheduleManager.java
+++ b/src/com/android/tv/dvr/DvrScheduleManager.java
@@ -24,7 +24,6 @@ import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
-import android.util.LongSparseArray;
import android.util.Range;
import com.android.tv.ApplicationSingletons;
@@ -35,7 +34,10 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.recorder.InputTaskScheduler;
import com.android.tv.util.CompositeComparator;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -88,9 +90,8 @@ public class DvrScheduleManager {
private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>();
// The inner map is a hash map from scheduled recording to its conflicting status, i.e.,
// the boolean value true denotes the schedule is just partially conflicting, which means
- // although there's conflictit, it might still be recorded partially.
- private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap =
- new HashMap<>();
+ // although there's conflict, it might still be recorded partially.
+ private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>();
private boolean mInitialized;
@@ -171,10 +172,9 @@ public class DvrScheduleManager {
mInputScheduleMap.remove(inputId);
}
}
- Map<ScheduledRecording, Boolean> conflictInfo =
- mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId);
if (conflictInfo != null) {
- conflictInfo.remove(schedule);
+ conflictInfo.remove(schedule.getId());
if (conflictInfo.isEmpty()) {
mInputConflictInfoMap.remove(inputId);
}
@@ -221,21 +221,11 @@ public class DvrScheduleManager {
mInputScheduleMap.remove(inputId);
}
// Update conflict list as well
- Map<ScheduledRecording, Boolean> conflictInfo =
- mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId);
if (conflictInfo != null) {
- // Compare ID because ScheduledRecording.equals() doesn't work if the state
- // is changed.
- ScheduledRecording oldSchedule = null;
- for (ScheduledRecording s : conflictInfo.keySet()) {
- if (s.getId() == schedule.getId()) {
- oldSchedule = s;
- break;
- }
- }
- if (oldSchedule != null) {
- conflictInfo.put(schedule, conflictInfo.get(oldSchedule));
- conflictInfo.remove(oldSchedule);
+ ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId());
+ if (oldConflictInfo != null) {
+ oldConflictInfo.schedule = schedule;
}
}
}
@@ -317,24 +307,25 @@ public class DvrScheduleManager {
List<ScheduledRecording> addedConflicts = new ArrayList<>();
List<ScheduledRecording> removedConflicts = new ArrayList<>();
for (String inputId : mInputScheduleMap.keySet()) {
- Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId);
Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>();
- if (oldConflictsInfo != null) {
- for (ScheduledRecording r : oldConflictsInfo.keySet()) {
- oldConflictMap.put(r.getId(), r);
+ if (oldConflictInfo != null) {
+ for (ConflictInfo conflictInfo : oldConflictInfo.values()) {
+ oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule);
}
}
- Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId);
- if (conflictInfo.isEmpty()) {
+ List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId);
+ if (conflicts.isEmpty()) {
mInputConflictInfoMap.remove(inputId);
} else {
- mInputConflictInfoMap.put(inputId, conflictInfo);
- List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet());
- for (ScheduledRecording r : conflicts) {
- if (oldConflictMap.remove(r.getId()) == null) {
- addedConflicts.add(r);
+ Map<Long, ConflictInfo> conflictInfos = new HashMap<>();
+ for (ConflictInfo conflictInfo : conflicts) {
+ conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo);
+ if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) {
+ addedConflicts.add(conflictInfo.schedule);
}
}
+ mInputConflictInfoMap.put(inputId, conflictInfos);
}
removedConflicts.addAll(oldConflictMap.values());
}
@@ -565,8 +556,7 @@ public class DvrScheduleManager {
}
/**
- * Returns list of all conflicting scheduled recordings with schedules belonging to {@code
- * seriesRecording}
+ * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording}
* recording.
* <p>
* Any empty list means there is no conflicts.
@@ -581,9 +571,18 @@ public class DvrScheduleManager {
if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
return Collections.emptyList();
}
- List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings(
+ List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings(
seriesRecording.getId());
- return getConflictingSchedules(input, schedulesForSeries);
+ List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>();
+ for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) {
+ if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) {
+ availableScheduledRecordingForSeries.add(scheduledRecording);
+ }
+ }
+ if (availableScheduledRecordingForSeries.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(input, availableScheduledRecordingForSeries);
}
/**
@@ -617,16 +616,16 @@ public class DvrScheduleManager {
* the given input.
*/
@NonNull
- private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) {
+ private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) {
SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId);
SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId);
if (!mInitialized || input == null) {
- return Collections.emptyMap();
+ return Collections.emptyList();
}
List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
if (schedules == null || schedules.isEmpty()) {
- return Collections.emptyMap();
+ return Collections.emptyList();
}
return getConflictingSchedulesInfo(schedules, input.getTunerCount());
}
@@ -645,8 +644,8 @@ public class DvrScheduleManager {
if (!mInitialized || input == null) {
return false;
}
- Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
- return conflicts != null && conflicts.containsKey(schedule);
+ Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
+ return conflicts != null && conflicts.containsKey(schedule.getId());
}
/**
@@ -664,8 +663,12 @@ public class DvrScheduleManager {
if (!mInitialized || input == null) {
return false;
}
- Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
- return conflicts != null && conflicts.getOrDefault(schedule, false);
+ Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
+ if (conflicts != null) {
+ ConflictInfo conflictInfo = conflicts.get(schedule.getId());
+ return conflictInfo != null && conflictInfo.partialConflict;
+ }
+ return false;
}
/**
@@ -813,15 +816,17 @@ public class DvrScheduleManager {
@VisibleForTesting
static List<ScheduledRecording> getConflictingSchedules(
List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
- List<ScheduledRecording> result = new ArrayList<>(
- getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet());
- Collections.sort(result, RESULT_COMPARATOR);
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ConflictInfo conflictInfo :
+ getConflictingSchedulesInfo(schedules, tunerCount, periods)) {
+ result.add(conflictInfo.schedule);
+ }
return result;
}
@VisibleForTesting
- static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
- List<ScheduledRecording> schedules, int tunerCount) {
+ static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules,
+ int tunerCount) {
return getConflictingSchedulesInfo(schedules, tunerCount, null);
}
@@ -836,13 +841,13 @@ public class DvrScheduleManager {
* to be partially recorded under the given schedules and tuner count {@code true},
* or not {@code false}.
*/
- private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
+ private static List<ConflictInfo> getConflictingSchedulesInfo(
List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
// Sort by the same order as that in InputTaskScheduler.
Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator());
List<ScheduledRecording> recordings = new ArrayList<>();
- Map<ScheduledRecording, Boolean> conflicts = new HashMap<>();
+ Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>();
Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>();
// Simulate InputTaskScheduler.
while (!schedulesToCheck.isEmpty()) {
@@ -853,26 +858,29 @@ public class DvrScheduleManager {
if (modified2OriginalSchedules.containsKey(schedule)) {
// Schedule has been modified, which means it's already conflicted.
// Modify its state to partially conflicted.
- conflicts.put(modified2OriginalSchedules.get(schedule), true);
+ ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule);
+ conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
}
} else {
ScheduledRecording candidate = findReplaceableRecording(recordings, schedule);
if (candidate != null) {
if (!modified2OriginalSchedules.containsKey(candidate)) {
- conflicts.put(candidate, true);
+ conflicts.put(candidate, new ConflictInfo(candidate, true));
}
recordings.remove(candidate);
recordings.add(schedule);
if (modified2OriginalSchedules.containsKey(schedule)) {
// Schedule has been modified, which means it's already conflicted.
// Modify its state to partially conflicted.
- conflicts.put(modified2OriginalSchedules.get(schedule), true);
+ ScheduledRecording originalSchedule =
+ modified2OriginalSchedules.get(schedule);
+ conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
}
} else {
if (!modified2OriginalSchedules.containsKey(schedule)) {
// if schedule has been modified, it's already conflicted.
// No need to add it again.
- conflicts.put(schedule, false);
+ conflicts.put(schedule, new ConflictInfo(schedule, false));
}
long earliestEndTime = getEarliestEndTime(recordings);
if (earliestEndTime < schedule.getEndTimeMs()) {
@@ -912,7 +920,14 @@ public class DvrScheduleManager {
}
}
}
- return conflicts;
+ List<ConflictInfo> result = new ArrayList<>(conflicts.values());
+ Collections.sort(result, new Comparator<ConflictInfo>() {
+ @Override
+ public int compare(ConflictInfo lhs, ConflictInfo rhs) {
+ return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
+ }
+ });
+ return result;
}
private static void removeFinishedRecordings(List<ScheduledRecording> recordings,
@@ -954,6 +969,17 @@ public class DvrScheduleManager {
return earliest;
}
+ @VisibleForTesting
+ static class ConflictInfo {
+ public ScheduledRecording schedule;
+ public boolean partialConflict;
+
+ ConflictInfo(ScheduledRecording schedule, boolean partialConflict) {
+ this.schedule = schedule;
+ this.partialConflict = partialConflict;
+ }
+ }
+
/**
* A listener which is notified the initialization of schedule manager.
*/
@@ -970,6 +996,9 @@ public class DvrScheduleManager {
public interface OnConflictStateChangeListener {
/**
* Called when the conflicting schedules change.
+ * <p>
+ * Note that this can be called before
+ * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called.
*
* @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise
* {@code false}.
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index a653b5f4..2d41d732 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -25,6 +25,7 @@ import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
@@ -36,8 +37,11 @@ import android.support.annotation.IntDef;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.tuner.tvinput.TunerTvInputService;
+import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.io.File;
@@ -294,7 +298,7 @@ public class DvrStorageStatusManager {
storageMounted, storageMountedDir, storageMountedCapacity);
}
- private class CleanUpDbTask extends AsyncTask<Void, Void, Void> {
+ private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> {
private final ContentResolver mContentResolver;
private CleanUpDbTask() {
@@ -302,13 +306,15 @@ public class DvrStorageStatusManager {
}
@Override
- protected Void doInBackground(Void... params) {
+ protected Boolean doInBackground(Void... params) {
@DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus();
if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
return null;
}
- List<ContentProviderOperation> ops = getDeleteOps(storageStatus
- == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL);
+ if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
+ return true;
+ }
+ List<ContentProviderOperation> ops = getDeleteOps();
if (ops == null || ops.isEmpty()) {
return null;
}
@@ -329,13 +335,28 @@ public class DvrStorageStatusManager {
}
@Override
- protected void onPostExecute(Void result) {
+ protected void onPostExecute(Boolean forgetStorage) {
+ if (forgetStorage != null && forgetStorage == true) {
+ DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager();
+ TvInputManagerHelper tvInputManagerHelper =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ List<TvInputInfo> tvInputInfoList =
+ tvInputManagerHelper.getTvInputInfos(true, false);
+ if (tvInputInfoList == null || tvInputInfoList.isEmpty()) {
+ return;
+ }
+ for (TvInputInfo info : tvInputInfoList) {
+ if (Utils.isBundledInput(info.getId())) {
+ dvrManager.forgetStorage(info.getId());
+ }
+ }
+ }
if (mCleanUpDbTask == this) {
mCleanUpDbTask = null;
}
}
- private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) {
+ private List<ContentProviderOperation> getDeleteOps() {
List<ContentProviderOperation> ops = new ArrayList<>();
try (Cursor c = mContentResolver.query(
@@ -364,7 +385,7 @@ public class DvrStorageStatusManager {
continue;
}
File recordedProgramDir = new File(dataUri.getPath());
- if (deleteAll || !recordedProgramDir.exists()) {
+ if (!recordedProgramDir.exists()) {
ops.add(ContentProviderOperation.newDelete(
TvContract.buildRecordedProgramUri(Long.parseLong(id))).build());
}
diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
index 4eada742..dc05ed06 100644
--- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java
+++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
@@ -22,6 +22,7 @@ import android.media.tv.TvInputManager;
import android.support.annotation.IntDef;
import com.android.tv.common.SharedPreferencesUtils;
+import com.android.tv.dvr.data.RecordedProgram;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index bf72d912..129ba153 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -18,7 +18,9 @@ package com.android.tv.dvr;
import android.support.annotation.MainThread;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
/**
* Full data manager.
@@ -27,7 +29,7 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState;
* for internal use only. Do not call them from UI directly.
*/
@MainThread
-interface WritableDvrDataManager extends DvrDataManager {
+public interface WritableDvrDataManager extends DvrDataManager {
/**
* Adds new recordings.
*/
diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java
index 0ed6362c..2ade1dad 100644
--- a/src/com/android/tv/dvr/IdGenerator.java
+++ b/src/com/android/tv/dvr/data/IdGenerator.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import java.util.concurrent.atomic.AtomicLong;
diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index dd744f80..18e1c769 100644
--- a/src/com/android/tv/dvr/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import static android.media.tv.TvContract.RecordedPrograms;
diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 2bda10ea..88849a2c 100644
--- a/src/com/android/tv/dvr/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import android.content.ContentValues;
import android.content.Context;
@@ -27,9 +27,11 @@ import android.text.TextUtils;
import android.util.Range;
import com.android.tv.R;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.util.CompositeComparator;
import com.android.tv.util.Utils;
@@ -683,6 +685,19 @@ public final class ScheduledRecording implements Parcelable {
}
}
+ /**
+ * Returns the program's display title, if the program title is not null, returns program title.
+ * Otherwise returns the channel name.
+ */
+ public String getProgramDisplayTitle(Context context) {
+ if (!TextUtils.isEmpty(mProgramTitle)) {
+ return mProgramTitle;
+ }
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(mChannelId);
+ return channel != null ? channel.getDisplayName()
+ : context.getString(R.string.no_program_information);
+ }
/**
* Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}.
diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java
new file mode 100644
index 00000000..89533dbb
--- /dev/null
+++ b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.data;
+
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * A plain java object which includes the season/episode number for the series recording.
+ */
+public class SeasonEpisodeNumber {
+ public final long seriesRecordingId;
+ public final String seasonNumber;
+ public final String episodeNumber;
+
+ /**
+ * Creates a new Builder with the values set from an existing {@link ScheduledRecording}.
+ */
+ public SeasonEpisodeNumber(ScheduledRecording r) {
+ this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
+ }
+
+ public SeasonEpisodeNumber(long seriesRecordingId, String seasonNumber, String episodeNumber) {
+ this.seriesRecordingId = seriesRecordingId;
+ this.seasonNumber = seasonNumber;
+ this.episodeNumber = episodeNumber;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SeasonEpisodeNumber)
+ || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) {
+ return false;
+ }
+ SeasonEpisodeNumber that = (SeasonEpisodeNumber) o;
+ return seriesRecordingId == that.seriesRecordingId
+ && Objects.equals(seasonNumber, that.seasonNumber)
+ && Objects.equals(episodeNumber, that.episodeNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
+ }
+
+ @Override
+ public String toString() {
+ return "SeasonEpisodeNumber{" +
+ "seriesRecordingId=" + seriesRecordingId +
+ ", seasonNumber='" + seasonNumber +
+ ", episodeNumber=" + episodeNumber +
+ '}';
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java
index 30256dc5..a0dec4a4 100644
--- a/src/com/android/tv/dvr/SeriesInfo.java
+++ b/src/com/android/tv/dvr/data/SeriesInfo.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
/**
* Series information.
diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index f0690f5f..b7cf0f66 100644
--- a/src/com/android/tv/dvr/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import android.content.ContentValues;
import android.database.Cursor;
@@ -26,6 +26,7 @@ import android.text.TextUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.Utils;
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index 1a12fb23..c5383d02 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -21,8 +21,8 @@ import android.database.Cursor;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.NamedThreadFactory;
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 2f16ba5d..8b9481a9 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -27,8 +27,8 @@ import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index df181455..8a0c2d19 100644
--- a/src/com/android/tv/dvr/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.provider;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@@ -34,6 +34,11 @@ import com.android.tv.TvApplication;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrDataManagerImpl;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
import com.android.tv.util.TvProviderUriMatcher;
@@ -57,11 +62,12 @@ import java.util.Set;
*/
@MainThread
@TargetApi(Build.VERSION_CODES.N)
-class DvrDbSync {
+public class DvrDbSync {
private static final String TAG = "DvrDbSync";
private static final boolean DEBUG = false;
private final Context mContext;
+ private final DvrManager mDvrManager;
private final DvrDataManagerImpl mDataManager;
private final ChannelDataManager mChannelDataManager;
private final Queue<Long> mProgramIdQueue = new LinkedList<>();
@@ -129,17 +135,21 @@ class DvrDbSync {
}
};
- DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
- this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager());
+ public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(),
+ TvApplication.getSingletons(context).getDvrManager(),
+ SeriesRecordingScheduler.getInstance(context));
}
@VisibleForTesting
DvrDbSync(Context context, DvrDataManagerImpl dataManager,
- ChannelDataManager channelDataManager) {
+ ChannelDataManager channelDataManager, DvrManager dvrManager,
+ SeriesRecordingScheduler seriesRecordingScheduler) {
mContext = context;
+ mDvrManager = dvrManager;
mDataManager = dataManager;
mChannelDataManager = channelDataManager;
- mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context);
+ mSeriesRecordingScheduler = seriesRecordingScheduler;
}
/**
@@ -279,10 +289,9 @@ class DvrDbSync {
mDataManager.getSeriesRecording(program.getSeriesId());
if (seriesRecording == null) {
// The new program is episodic while the previous one isn't.
- SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext)
- .getDvrManager().addSeriesRecording(program,
- Collections.singletonList(program),
- SeriesRecording.STATE_SERIES_STOPPED);
+ SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording(
+ program, Collections.singletonList(program),
+ SeriesRecording.STATE_SERIES_STOPPED);
builder.setSeriesRecordingId(newSeriesRecording.getId());
needUpdate = true;
} else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
@@ -306,8 +315,9 @@ class DvrDbSync {
// Old program belongs to a series but the new one doesn't.
seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
}
- // Change start time only when the recording start time has not passed.
- boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs
+ // Change start time only when the recording is not started yet.
+ boolean needToChangeStartTime =
+ schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
&& program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
if (needToChangeStartTime) {
builder.setStartTimeMs(program.getStartTimeUtcMillis());
diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index 15ca2700..ba0aca51 100644
--- a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.provider;
import android.annotation.TargetApi;
import android.content.Context;
@@ -24,13 +24,15 @@ import android.media.tv.TvContract.Programs;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
import com.android.tv.util.AsyncDbTask.CursorFilter;
import com.android.tv.util.PermissionUtils;
@@ -40,7 +42,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Objects;
import java.util.Set;
/**
@@ -253,21 +254,13 @@ abstract public class EpisodicProgramLoadTask {
return sqlParams;
}
- @VisibleForTesting
- static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes,
- ScheduledEpisode episode) {
- // The episode whose season number or episode number is null will always be scheduled.
- return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber)
- && !TextUtils.isEmpty(episode.episodeNumber);
- }
-
/**
* Filter the programs which match the series recording. The episodes which the schedules are
* already created for are filtered out too.
*/
private class SeriesRecordingCursorFilter implements CursorFilter {
private final Set<Long> mDisallowedProgramIds = new HashSet<>();
- private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>();
+ private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>();
SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) {
if (!mLoadDisallowedProgram) {
@@ -282,7 +275,7 @@ abstract public class EpisodicProgramLoadTask {
if (seriesRecordingIds.contains(r.getSeriesRecordingId())
&& r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
&& r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
- mScheduledEpisodes.add(new ScheduledEpisode(r));
+ mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r));
}
}
}
@@ -306,9 +299,9 @@ abstract public class EpisodicProgramLoadTask {
}
if (programMatches) {
return mLoadScheduledEpisode
- || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode(
- seriesRecording.getId(), program.getSeasonNumber(),
- program.getEpisodeNumber()));
+ || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber(
+ seriesRecording.getId(), program.getSeasonNumber(),
+ program.getEpisodeNumber()));
}
}
return false;
@@ -333,50 +326,4 @@ abstract public class EpisodicProgramLoadTask {
public String[] selectionArgs;
public CursorFilter filter;
}
-
- /**
- * A plain java object which includes the season/episode number for the series recording.
- */
- public static class ScheduledEpisode {
- public final long seriesRecordingId;
- public final String seasonNumber;
- public final String episodeNumber;
-
- /**
- * Create a new Builder with the values set from an existing {@link ScheduledRecording}.
- */
- ScheduledEpisode(ScheduledRecording r) {
- this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
- }
-
- public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) {
- this.seriesRecordingId = seriesRecordingId;
- this.seasonNumber = seasonNumber;
- this.episodeNumber = episodeNumber;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof ScheduledEpisode)) return false;
- ScheduledEpisode that = (ScheduledEpisode) o;
- return seriesRecordingId == that.seriesRecordingId
- && Objects.equals(seasonNumber, that.seasonNumber)
- && Objects.equals(episodeNumber, that.episodeNumber);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
- }
-
- @Override
- public String toString() {
- return "ScheduledEpisode{" +
- "seriesRecordingId=" + seriesRecordingId +
- ", seasonNumber='" + seasonNumber +
- ", episodeNumber=" + episodeNumber +
- '}';
- }
- }
}
diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java
index 201e379e..8aa90116 100644
--- a/src/com/android/tv/dvr/ConflictChecker.java
+++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
import android.content.ContentUris;
@@ -37,6 +37,9 @@ import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
index 8c40aaa8..08ffaf86 100644
--- a/src/com/android/tv/dvr/DvrRecordingService.java
+++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
@@ -14,9 +14,10 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.app.AlarmManager;
+import android.app.Notification;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@@ -27,9 +28,13 @@ import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.tv.ApplicationSingletons;
+import com.android.tv.InputSessionManager;
+import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener;
+import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.dvr.WritableDvrDataManager;
import com.android.tv.util.Clock;
import com.android.tv.util.RecurringRunner;
@@ -52,6 +57,8 @@ public class DvrRecordingService extends Service {
private static final boolean DEBUG = false;
public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler";
+ private static final int ONGOING_NOTIFICATION_ID = 1;
+
public static void startService(Context context) {
Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class);
context.startService(dvrSchedulerIntent);
@@ -62,6 +69,27 @@ public class DvrRecordingService extends Service {
private Scheduler mScheduler;
private HandlerThread mHandlerThread;
+ private InputSessionManager mSessionManager;
+ private boolean mForeground;
+
+ private final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener =
+ new OnRecordingSessionChangeListener() {
+ @Override
+ public void onRecordingSessionChange(final boolean create, final int count) {
+ if (create && !mForeground) {
+ Notification notification =
+ new Notification.Builder(getApplicationContext())
+ .setContentTitle(TAG)
+ .setSmallIcon(R.drawable.ic_dvr)
+ .build();
+ startForeground(ONGOING_NOTIFICATION_ID, notification);
+ mForeground = true;
+ } else if (!create && mForeground && count == 0) {
+ stopForeground(STOP_FOREGROUND_REMOVE);
+ mForeground = false;
+ }
+ }
+ };
@Override
public void onCreate() {
@@ -70,8 +98,11 @@ public class DvrRecordingService extends Service {
super.onCreate();
SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
ApplicationSingletons singletons = TvApplication.getSingletons(this);
- WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager();
+ WritableDvrDataManager dataManager =
+ (WritableDvrDataManager) singletons.getDvrDataManager();
+ mSessionManager = singletons.getInputSessionManager();
+ mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
// mScheduler may have been set for testing.
if (mScheduler == null) {
@@ -105,6 +136,7 @@ public class DvrRecordingService extends Service {
mHandlerThread.quitSafely();
mHandlerThread = null;
}
+ mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
super.onDestroy();
}
diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
index 6d2f0d43..8c6ee145 100644
--- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java
+++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import com.android.tv.TvApplication;
diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 53c89ebc..46546a76 100644
--- a/src/com/android/tv/dvr/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.content.Context;
import android.media.tv.TvInputInfo;
@@ -30,6 +30,10 @@ import android.util.LongSparseArray;
import com.android.tv.InputSessionManager;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Clock;
import com.android.tv.util.CompositeComparator;
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index c3d236b0..c3314dde 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
import android.content.Context;
@@ -37,7 +37,10 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
-import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper;
import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
@@ -256,13 +259,21 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
public void run() {
if (TvApplication.getSingletons(mContext).getMainActivityWrapper()
.isResumed()) {
- Toast.makeText(mContext.getApplicationContext(),
- R.string.dvr_error_insufficient_space_description,
- Toast.LENGTH_LONG)
- .show();
+ ScheduledRecording scheduledRecording = mDataManager
+ .getScheduledRecording(mScheduledRecording.getId());
+ if (scheduledRecording != null) {
+ Toast.makeText(mContext.getApplicationContext(),
+ mContext.getString(R.string
+ .dvr_error_insufficient_space_description_one_recording,
+ scheduledRecording.getProgramDisplayTitle(mContext)),
+ Toast.LENGTH_LONG)
+ .show();
+ }
} else {
Utils.setRecordingFailedReason(mContext.getApplicationContext(),
TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(),
+ mScheduledRecording.getProgramDisplayTitle(mContext));
}
}
});
diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
index cd79a631..d958c4a1 100644
--- a/src/com/android/tv/dvr/ScheduledProgramReaper.java
+++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.Clock;
import java.util.ArrayList;
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/recorder/Scheduler.java
index ce78e1be..19e73342 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/recorder/Scheduler.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.app.AlarmManager;
import android.app.PendingIntent;
@@ -32,8 +32,12 @@ import android.util.Range;
import com.android.tv.InputSessionManager;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ChannelDataManager.Listener;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Clock;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 5ed12ce8..8a211f66 100644
--- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@@ -23,7 +23,6 @@ import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.MainThread;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -36,9 +35,16 @@ import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
-import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesInfo;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
import com.android.tv.experiments.Experiments;
import java.util.ArrayList;
@@ -52,11 +58,11 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.concurrent.CopyOnWriteArraySet;
import java.util.Set;
/**
- * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}.
+ * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for
+ * the {@link com.android.tv.dvr.data.SeriesRecording}.
* <p>
* The current implementation assumes that the series recordings are scheduled only for one channel.
*/
@@ -85,15 +91,13 @@ public class SeriesRecordingScheduler {
private final DvrManager mDvrManager;
private final WritableDvrDataManager mDataManager;
private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>();
- private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>();
+ private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks =
+ new LongSparseArray<>();
private final Set<String> mFetchedSeriesIds = new ArraySet<>();
private final SharedPreferences mSharedPreferences;
private boolean mStarted;
private boolean mPaused;
private final Set<Long> mPendingSeriesRecordings = new ArraySet<>();
- private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners =
- new CopyOnWriteArraySet<>();
-
private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() {
@Override
@@ -107,7 +111,7 @@ public class SeriesRecordingScheduler {
public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
// Cancel the update.
for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
- iter.hasNext(); ) {
+ iter.hasNext(); ) {
SeriesRecordingUpdateTask task = iter.next();
if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings,
SeriesRecording.ID_COMPARATOR).isEmpty()) {
@@ -115,6 +119,13 @@ public class SeriesRecordingScheduler {
iter.remove();
}
}
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId());
+ if (task != null) {
+ task.cancel(true);
+ mFetchSeriesInfoTasks.remove(seriesRecording.getId());
+ }
+ }
}
@Override
@@ -226,7 +237,8 @@ public class SeriesRecordingScheduler {
}
if (DEBUG) Log.d(TAG, "stop");
mStarted = false;
- for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) {
+ for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) {
+ FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i));
task.cancel(true);
}
mFetchSeriesInfoTasks.clear();
@@ -250,7 +262,7 @@ public class SeriesRecordingScheduler {
if (Experiments.CLOUD_EPG.get()) {
FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording);
task.execute();
- mFetchSeriesInfoTasks.add(task);
+ mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
}
}
@@ -363,20 +375,6 @@ public class SeriesRecordingScheduler {
}
}
- /**
- * Adds {@link OnSeriesRecordingUpdatedListener}.
- */
- public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
- mOnSeriesRecordingUpdatedListeners.add(listener);
- }
-
- /**
- * Removes {@link OnSeriesRecordingUpdatedListener}.
- */
- public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
- mOnSeriesRecordingUpdatedListeners.remove(listener);
- }
-
private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) {
for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) {
@@ -403,8 +401,7 @@ public class SeriesRecordingScheduler {
/**
* @see #pickOneProgramPerEpisode(List, List)
*/
- @VisibleForTesting
- static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
+ public static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
DvrDataManager dataManager, List<SeriesRecording> seriesRecordings,
List<Program> programs) {
// Initialize.
@@ -415,7 +412,7 @@ public class SeriesRecordingScheduler {
seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId());
}
// Group programs by the episode.
- Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>();
+ Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>();
for (Program program : programs) {
long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId());
if (TextUtils.isEmpty(program.getSeasonNumber())
@@ -424,17 +421,17 @@ public class SeriesRecordingScheduler {
result.get(seriesRecordingId).add(program);
continue;
}
- ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId,
+ SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId,
program.getSeasonNumber(), program.getEpisodeNumber());
- List<Program> programsForEpisode = programsForEpisodeMap.get(episode);
+ List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber);
if (programsForEpisode == null) {
programsForEpisode = new ArrayList<>();
- programsForEpisodeMap.put(episode, programsForEpisode);
+ programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode);
}
programsForEpisode.add(program);
}
// Pick one program.
- for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) {
+ for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) {
List<Program> programsForEpisode = entry.getValue();
Collections.sort(programsForEpisode, new Comparator<Program>() {
@Override
@@ -512,13 +509,6 @@ public class SeriesRecordingScheduler {
mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
}
}
- if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) {
- for (OnSeriesRecordingUpdatedListener listener
- : mOnSeriesRecordingUpdatedListeners) {
- listener.onSeriesRecordingUpdated(
- SeriesRecording.toArray(getSeriesRecordings()));
- }
- }
}
@Override
@@ -561,19 +551,12 @@ public class SeriesRecordingScheduler {
mFetchedSeriesIds.add(seriesInfo.getId());
updateFetchedSeries();
}
- mFetchSeriesInfoTasks.remove(this);
+ mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
}
@Override
protected void onCancelled(SeriesInfo seriesInfo) {
- mFetchSeriesInfoTasks.remove(this);
+ mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
}
}
-
- /**
- * A listener to notify when series recording are updated.
- */
- public interface OnSeriesRecordingUpdatedListener {
- void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings);
- }
}
diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java
new file mode 100644
index 00000000..ec3b5065
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/BigArguments.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.support.annotation.NonNull;
+
+import com.android.tv.common.SoftPreconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Stores the object to pass through activities/fragments.
+ */
+public class BigArguments {
+ private final static String TAG = "BigArguments";
+ private static Map<String, Object> sBigArgumentMap = new HashMap<>();
+
+ /**
+ * Sets the argument.
+ */
+ public static void setArgument(String name, @NonNull Object value) {
+ SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null");
+ sBigArgumentMap.put(name, value);
+ }
+
+ /**
+ * Returns the argument which is associated to the name.
+ */
+ public static Object getArgument(String name) {
+ return sBigArgumentMap.get(name);
+ }
+
+ /**
+ * Resets the arguments.
+ */
+ public static void reset() {
+ sBigArgumentMap.clear();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
new file mode 100644
index 00000000..cddece73
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.transition.ChangeImageTransform;
+import android.transition.TransitionValues;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import com.android.tv.R;
+
+import java.util.Map;
+
+/**
+ * TODO: Remove this class once b/32405620 is fixed.
+ * This class is for the workaround of b/32405620 and only for the shared element transition between
+ * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and
+ * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}.
+ */
+public class ChangeImageTransformWithScaledParent extends ChangeImageTransform {
+ private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
+
+ public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ applyParentScale(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(TransitionValues transitionValues) {
+ super.captureEndValues(transitionValues);
+ applyParentScale(transitionValues);
+ }
+
+ private void applyParentScale(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ Map<String, Object> values = transitionValues.values;
+ Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX);
+ if (matrix != null && view.getId() == R.id.details_overview_image
+ && view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ if (imageView.getScaleType() == ScaleType.CENTER_INSIDE
+ && imageView.getDrawable() instanceof BitmapDrawable) {
+ Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
+ if (bitmap.getWidth() < imageView.getWidth()
+ && bitmap.getHeight() < imageView.getHeight()) {
+ float scale = imageView.getContext().getResources().getFraction(
+ R.fraction.lb_focus_zoom_factor_medium, 1, 1);
+ matrix.postScale(scale, scale, imageView.getWidth() / 2,
+ imageView.getHeight() / 2);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
deleted file mode 100644
index 5d8e20ff..00000000
--- a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.dvr.ui;
-
-import android.content.res.Resources;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrManager;
-
-/**
- * {@link RecordingDetailsFragment} for current recording in DVR.
- */
-public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
- private static final int ACTION_STOP_RECORDING = 1;
-
- @Override
- protected SparseArrayObjectAdapter onCreateActionsAdapter() {
- SparseArrayObjectAdapter adapter =
- new SparseArrayObjectAdapter(new ActionPresenterSelector());
- Resources res = getResources();
- adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING,
- res.getString(R.string.epg_dvr_dialog_message_stop_recording), null,
- res.getDrawable(R.drawable.lb_ic_stop)));
- return adapter;
- }
-
- @Override
- protected OnActionClickedListener onCreateOnActionClickedListener() {
- return new OnActionClickedListener() {
- @Override
- public void onActionClicked(Action action) {
- if (action.getId() == ACTION_STOP_RECORDING) {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity())
- .getDvrManager();
- dvrManager.stopRecording(getRecording());
- }
- getActivity().finish();
- }
- };
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index 9df228d1..936e9c31 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -28,11 +28,9 @@ import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.util.Utils;
+import com.android.tv.dvr.data.RecordedProgram;
import java.util.List;
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 78f21784..3c73cb47 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
@@ -25,15 +25,12 @@ import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.text.format.DateUtils;
-import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.util.Utils;
+import com.android.tv.dvr.data.ScheduledRecording;
import java.util.List;
diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
index 837d8ab2..880dc8ac 100644
--- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
@@ -27,7 +27,7 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment;
import java.util.ArrayList;
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index e7be4d0a..5985f56f 100644
--- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
@@ -34,10 +34,9 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
-import com.android.tv.dvr.ConflictChecker;
-import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.recorder.ConflictChecker;
+import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Utils;
import java.util.ArrayList;
diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
deleted file mode 100644
index 73ddcdd0..00000000
--- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.text.TextUtils;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrManager;
-
-import java.util.List;
-
-public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment {
- private static final int ACTION_CANCEL = 1;
- private static final int ACTION_FORGET_STORAGE = 2;
- private String mInputId;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Bundle args = getArguments();
- if (args != null) {
- mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID);
- }
- SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId));
- super.onCreate(savedInstanceState);
- }
-
- @NonNull
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.dvr_error_forget_storage_title);
- String description = getResources().getString(
- R.string.dvr_error_forget_storage_description);
- return new Guidance(title, description, null, null);
- }
-
- @Override
- public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_CANCEL)
- .title(getResources().getString(R.string.dvr_action_error_cancel))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_FORGET_STORAGE)
- .title(getResources().getString(R.string.dvr_action_error_forget_storage))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() != ACTION_FORGET_STORAGE) {
- dismissDialog();
- return;
- }
- DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
- dvrManager.forgetStorage(mInputId);
- Activity activity = getActivity();
- if (activity instanceof DvrDetailsActivity) {
- // Since we removed everything, just finish the activity.
- activity.finish();
- } else {
- dismissDialog();
- }
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index d26e6836..433588da 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -16,10 +16,12 @@
package com.android.tv.dvr.ui;
+import android.app.Activity;
import android.app.DialogFragment;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidanceStylist;
import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.VerticalGridView;
import android.view.LayoutInflater;
@@ -29,11 +31,26 @@ import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener;
+
+import java.util.List;
public class DvrGuidedStepFragment extends GuidedStepFragment {
+ /**
+ * Action ID for "recording/scheduling the program anyway".
+ */
+ public static final int ACTION_RECORD_ANYWAY = 1;
+ /**
+ * Action ID for "deleting existed recordings".
+ */
+ public static final int ACTION_DELETE_RECORDINGS = 2;
+ /**
+ * Action ID for "cancelling current recording request".
+ */
+ public static final int ACTION_CANCEL_RECORDING = 3;
+
private DvrManager mDvrManager;
private OnActionClickListener mOnActionClickListener;
@@ -86,4 +103,35 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
protected void setOnActionClickListener(OnActionClickListener listener) {
mOnActionClickListener = listener;
}
+
+ /**
+ * The inner guided step fragment for
+ * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
+ * .DvrNoFreeSpaceErrorDialogFragment}.
+ */
+ public static class DvrNoFreeSpaceErrorFragment
+ extends DvrGuidedStepFragment {
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title),
+ getString(R.string.dvr_error_no_free_space_description), null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_RECORD_ANYWAY)
+ .title(R.string.dvr_action_record_anyway)
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_DELETE_RECORDINGS)
+ .title(R.string.dvr_action_delete_recordings)
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_CANCEL_RECORDING)
+ .title(R.string.dvr_action_record_cancel)
+ .build());
+ }
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
index 2b132db8..9054dd03 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -29,6 +29,7 @@ import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.guide.ProgramGuide;
@@ -166,6 +167,17 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
}
/**
+ * A dialog fragment to show error message when there is no enough free space to record.
+ */
+ public static class DvrNoFreeSpaceErrorDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment();
+ }
+ }
+
+ /**
* A dialog fragment to show error message when the current storage is too small to
* support DVR
*/
diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
index 3b1dbfa0..3c5df1a6 100644
--- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
@@ -17,6 +17,7 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
@@ -24,19 +25,67 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import java.util.ArrayList;
import java.util.List;
public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
- private static final int ACTION_DONE = 1;
- private static final int ACTION_OPEN_DVR = 2;
+ /**
+ * Key for the failed scheduled recordings information.
+ */
+ public static final String FAILED_SCHEDULED_RECORDING_INFOS =
+ "failed_scheduled_recording_infos";
+
+ private static final String TAG = "DvrInsufficientSpaceErrorFragment";
+
+ private static final int ACTION_VIEW_RECENT_RECORDINGS = 1;
+
+ private ArrayList<String> mFailedScheduledRecordingInfos;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Bundle args = getArguments();
+ if (args != null) {
+ mFailedScheduledRecordingInfos =
+ args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS);
+ }
+ SoftPreconditions.checkState(
+ mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(),
+ TAG, "failed scheduled recording is null");
+ }
@Override
public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.dvr_error_insufficient_space_title);
- String description = getResources()
- .getString(R.string.dvr_error_insufficient_space_description);
+ String title;
+ String description;
+ int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size();
+ if (failedScheduledRecordingSize == 1) {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_one_recording,
+ mFailedScheduledRecordingInfos.get(0));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_one_recording,
+ mFailedScheduledRecordingInfos.get(0));
+ } else if (failedScheduledRecordingSize == 2) {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_two_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_two_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1));
+ } else {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_three_or_more_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1),
+ mFailedScheduledRecordingInfos.get(2));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_three_or_more_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1),
+ mFailedScheduledRecordingInfos.get(2));
+ }
return new Guidance(title, description, null, null);
}
@@ -44,26 +93,21 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
Activity activity = getActivity();
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_DONE)
- .title(getResources().getString(R.string.dvr_action_error_done))
+ .clickAction(GuidedAction.ACTION_ID_OK)
.build());
- DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext())
- .getDvrDataManager();
- if (!(dvrDataManager.getRecordedPrograms().isEmpty()
- && dvrDataManager.getStartedRecordings().isEmpty()
- && dvrDataManager.getNonStartedScheduledRecordings().isEmpty()
- && dvrDataManager.getSeriesRecordings().isEmpty())) {
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_OPEN_DVR)
- .title(getResources().getString(R.string.dvr_action_error_open_dvr))
- .build());
+ if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) {
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_VIEW_RECENT_RECORDINGS)
+ .title(getResources().getString(
+ R.string.dvr_error_insufficient_space_action_view_recent_recordings))
+ .build());
}
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_OPEN_DVR) {
- Intent intent = new Intent(getActivity(), DvrActivity.class);
+ if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) {
+ Intent intent = new Intent(getActivity(), DvrBrowseActivity.class);
getActivity().startActivity(intent);
}
dismissDialog();
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index 2e2c2849..8dc9eb4e 100644
--- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
@@ -17,29 +17,27 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
import android.os.Bundle;
-import android.support.v17.leanback.app.GuidedStepFragment;
+import android.provider.Settings;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
-import android.text.TextUtils;
+import android.util.Log;
import com.android.tv.R;
-import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
import java.util.List;
public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
- private static final int ACTION_CANCEL = 1;
- private static final int ACTION_FORGET_STORAGE = 2;
- private String mInputId;
+ private static String TAG = "DvrMissingStorageErrorFragment";
+
+ private static final int ACTION_OK = 1;
+ private static final int ACTION_OPEN_STORAGE_SETTINGS = 2;
@Override
public void onCreate(Bundle savedInstanceState) {
- Bundle args = getArguments();
- if (args != null) {
- mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID);
- }
- SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId));
super.onCreate(savedInstanceState);
}
@@ -55,25 +53,31 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
Activity activity = getActivity();
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_CANCEL)
- .title(getResources().getString(R.string.dvr_action_error_cancel))
+ .id(ACTION_OK)
+ .title(android.R.string.ok)
.build());
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_FORGET_STORAGE)
- .title(getResources().getString(R.string.dvr_action_error_forget_storage))
+ .id(ACTION_OPEN_STORAGE_SETTINGS)
+ .title(getResources().getString(R.string.dvr_action_error_storage_settings))
.build());
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_FORGET_STORAGE) {
- DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment();
- Bundle args = new Bundle();
- args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId);
- fragment.setArguments(args);
- GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ Activity activity = getActivity();
+ if (activity instanceof DvrDetailsActivity) {
+ activity.finish();
+ } else {
+ dismissDialog();
+ }
+ if (action.getId() != ACTION_OPEN_STORAGE_SETTINGS) {
return;
}
- dismissDialog();
+ final Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "Can't start internal storage settings activity", e);
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
index 158bd824..562898a3 100644
--- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
@@ -33,7 +33,7 @@ import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.ArrayList;
import java.util.List;
@@ -41,7 +41,7 @@ import java.util.List;
/**
* Fragment for DVR series recording settings.
*/
-public class PrioritySettingsFragment extends GuidedStepFragment {
+public class DvrPrioritySettingsFragment extends GuidedStepFragment {
/**
* Name of series recording id starting the fragment.
* Type: Long
@@ -162,7 +162,6 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
return;
}
if (action.getId() < 0) {
- int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording);
mSelectedRecording = null;
for (int i = 0; i < mSeriesRecordings.size(); ++i) {
updateItem(i);
@@ -248,4 +247,4 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL);
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index da6d1637..d6008315 100644
--- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
@@ -32,9 +32,8 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.util.Utils;
@@ -48,18 +47,26 @@ import java.util.List;
*/
@TargetApi(Build.VERSION_CODES.N)
public class DvrScheduleFragment extends DvrGuidedStepFragment {
+ /**
+ * Key for the whether to add the current program to series.
+ * Type: boolean
+ */
+ public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series";
+
private static final String TAG = "DvrScheduleFragment";
private static final int ACTION_RECORD_EPISODE = 1;
private static final int ACTION_RECORD_SERIES = 2;
private Program mProgram;
+ private boolean mAddCurrentProgramToSeries;
@Override
public void onCreate(Bundle savedInstanceState) {
Bundle args = getArguments();
if (args != null) {
mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM);
+ mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false);
}
DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG,
@@ -139,8 +146,10 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
.build();
getDvrManager().updateSeriesRecording(seriesRecording);
}
+
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- seriesRecording.getId(), null, true, true, true);
+ seriesRecording.getId(), null, true, true, true,
+ mAddCurrentProgramToSeries ? mProgram : null);
dismissDialog();
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index f57e4b05..667af34a 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -22,9 +22,6 @@ import android.support.v17.leanback.app.GuidedStepFragment;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.ui.SeriesDeletionFragment;
-import com.android.tv.ui.sidepanel.SettingsFragment;
/**
* Activity to show details view in DVR.
@@ -42,7 +39,7 @@ public class DvrSeriesDeletionActivity extends Activity {
setContentView(R.layout.activity_dvr_series_settings);
// Check savedInstanceState to prevent that activity is being showed with animation.
if (savedInstanceState == null) {
- SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment();
+ DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment();
deletionFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
}
diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 36e3cfc1..8bf8560f 100644
--- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -33,9 +33,10 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.ui.GuidedActionsStylistWithDivider;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -47,7 +48,7 @@ import java.util.concurrent.TimeUnit;
/**
* Fragment for DVR series recording settings.
*/
-public class SeriesDeletionFragment extends GuidedStepFragment {
+public class DvrSeriesDeletionFragment extends GuidedStepFragment {
private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
// Since recordings' IDs are used as its check actions' IDs, which are random positive numbers,
@@ -218,8 +219,8 @@ public class SeriesDeletionFragment extends GuidedStepFragment {
private String getWatchedString(long watchedPositionMs, long durationMs) {
if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
return getResources().getString(R.string.dvr_series_watched_info_minutes,
- Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)),
- TimeUnit.MILLISECONDS.toMinutes(durationMs));
+ Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)),
+ Utils.getRoundOffMinsFromMs(durationMs));
} else {
return getResources().getString(R.string.dvr_series_watched_info_seconds,
Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)),
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index 1173df46..8f880f16 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -25,22 +25,29 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.data.Program;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import java.util.List;
public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
+ /**
+ * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}.
+ * Type: List<{@link Program}>
+ */
+ public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs";
+
private final static long SERIES_RECORDING_ID_NOT_SET = -1;
private final static int ACTION_VIEW_SCHEDULES = 1;
- private DvrScheduleManager mDvrScheduleManager;
private SeriesRecording mSeriesRecording;
private boolean mShowViewScheduleOption;
+ private List<Program> mPrograms;
private int mSchedulesAddedCount = 0;
private boolean mHasConflict = false;
@@ -58,22 +65,25 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
}
mShowViewScheduleOption = getArguments().getBoolean(
DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION);
- mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager();
mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager()
.getSeriesRecording(seriesRecordingId);
if (mSeriesRecording == null) {
getActivity().finish();
return;
}
+ mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS);
+ BigArguments.reset();
mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager()
.getAvailableScheduledRecording(mSeriesRecording.getId()).size();
+ DvrScheduleManager dvrScheduleManager =
+ TvApplication.getSingletons(context).getDvrScheduleManager();
List<ScheduledRecording> conflictingRecordings =
- mDvrScheduleManager.getConflictingSchedules(mSeriesRecording);
+ dvrScheduleManager.getConflictingSchedules(mSeriesRecording);
mHasConflict = !conflictingRecordings.isEmpty();
for (ScheduledRecording recording : conflictingRecordings) {
if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
++mInThisSeriesConflictCount;
- } else {
+ } else if (recording.getPriority() < mSeriesRecording.getPriority()) {
++mOutThisSeriesConflictCount;
}
}
@@ -113,6 +123,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
.TYPE_SERIES_SCHEDULE);
intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING,
mSeriesRecording);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms);
startActivity(intent);
}
getActivity().finish();
@@ -121,30 +134,30 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
private String getDescription() {
if (!mHasConflict) {
return getResources().getQuantityString(
- R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount,
+ R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount,
mSchedulesAddedCount, mSeriesRecording.getTitle());
} else {
// mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means
// mHasConflict is false. So we don't need to check that case.
if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_this_and_other_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_this_and_other_series_conflict,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mInThisSeriesConflictCount + mOutThisSeriesConflictCount);
} else if (mInThisSeriesConflictCount != 0) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_this_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_recording_scheduled_only_this_series_conflict,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mInThisSeriesConflictCount);
} else {
if (mOutThisSeriesConflictCount == 1) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_other_series_one_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_only_other_series_one_conflict,
mSchedulesAddedCount, mSchedulesAddedCount,
mSeriesRecording.getTitle());
} else {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_other_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_only_other_series_many_conflicts,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mOutThisSeriesConflictCount);
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
index 3f7671b3..6dd20b3a 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -17,7 +17,6 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
-import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
@@ -38,25 +37,34 @@ public class DvrSeriesSettingsActivity extends Activity {
/**
* Name of the boolean flag to decide if the series recording with empty schedule and recording
* will be removed.
+ * Type: boolean
*/
public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording";
/**
* Name of the boolean flag to decide if the setting fragment should be translucent.
+ * Type: boolean
*/
public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent";
/**
- * Name of the channel id list. If the channel list is given, we show the channels
- * from the values in channel option.
- * Type: Long array
+ * Name of the program list. The list contains the programs which belong to the series.
+ * Type: List<{@link com.android.tv.data.Program}>
*/
- public static final String CHANNEL_ID_LIST = "channel_id_list";
+ public static final String PROGRAM_LIST = "program_list";
/**
* Name of the boolean flag to check if the confirm dialog should show view schedule option.
+ * Type: boolean
*/
public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG =
"show_view_schedule_option_in_dialog";
+ /**
+ * Name of the current program added to series. The current program will be recorded only when
+ * the series recording is initialized from media controller. But for other case, the current
+ * program won't be recorded.
+ */
+ public static final String CURRENT_PROGRAM = "current_program";
+
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
@@ -66,7 +74,7 @@ public class DvrSeriesSettingsActivity extends Activity {
SoftPreconditions.checkArgument(seriesRecordingId != -1);
if (savedInstanceState == null) {
- SeriesSettingsFragment settingFragment = new SeriesSettingsFragment();
+ DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment();
settingFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame);
}
diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
index 6c05c9c6..f28382da 100644
--- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
@@ -17,19 +17,13 @@
package com.android.tv.dvr.ui;
import android.app.FragmentManager;
-import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.util.Log;
import android.util.LongSparseArray;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ProgressBar;
import com.android.tv.R;
import com.android.tv.TvApplication;
@@ -38,14 +32,14 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.SeriesRecording.ChannelOption;
-import com.android.tv.dvr.SeriesRecordingScheduler;
-import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.data.SeriesRecording.ChannelOption;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+
import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -53,7 +47,7 @@ import java.util.Set;
/**
* Fragment for DVR series recording settings.
*/
-public class SeriesSettingsFragment extends GuidedStepFragment
+public class DvrSeriesSettingsFragment extends GuidedStepFragment
implements DvrDataManager.SeriesRecordingListener {
private static final String TAG = "SeriesSettingsFragment";
private static final boolean DEBUG = false;
@@ -66,15 +60,13 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500;
private DvrDataManager mDvrDataManager;
- private ChannelDataManager mChannelDataManager;
- private DvrManager mDvrManager;
private SeriesRecording mSeriesRecording;
private long mSeriesRecordingId;
@ChannelOption int mChannelOption;
- private Comparator<Channel> mChannelComparator;
private long mSelectedChannelId;
private int mBackStackCount;
private boolean mShowViewScheduleOptionInDialog;
+ private Program mCurrentProgram;
private String mFragmentTitle;
private String mProrityActionTitle;
@@ -84,7 +76,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private String mChannelsActionAllText;
private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>();
private List<Channel> mChannels = new ArrayList<>();
- private EpisodicProgramLoadTask mEpisodicProgramLoadTask;
+ private List<Program> mPrograms;
private GuidedAction mPriorityGuidedAction;
private GuidedAction mChannelsGuidedAction;
@@ -100,22 +92,24 @@ public class SeriesSettingsFragment extends GuidedStepFragment
getActivity().finish();
return;
}
- mDvrManager = TvApplication.getSingletons(context).getDvrManager();
mShowViewScheduleOptionInDialog = getArguments().getBoolean(
DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG);
+ mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM);
mDvrDataManager.addSeriesRecordingListener(this);
- long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST);
- mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
- if (channelIds == null) {
- Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
- if (channel != null) {
- mId2Channel.put(channel.getId(), channel);
- mChannels.add(channel);
- }
- collectChannelsInBackground();
- } else {
- for (long channelId : channelIds) {
- Channel channel = mChannelDataManager.getChannel(channelId);
+ mPrograms = (List<Program>) BigArguments.getArgument(
+ DvrSeriesSettingsActivity.PROGRAM_LIST);
+ BigArguments.reset();
+ if (mPrograms == null) {
+ getActivity().finish();
+ return;
+ }
+ Set<Long> channelIds = new HashSet<>();
+ ChannelDataManager channelDataManager =
+ TvApplication.getSingletons(context).getChannelDataManager();
+ for (Program program : mPrograms) {
+ long channelId = program.getChannelId();
+ if (channelIds.add(channelId)) {
+ Channel channel = channelDataManager.getChannel(channelId);
if (channel != null) {
mId2Channel.put(channel.getId(), channel);
mChannels.add(channel);
@@ -125,16 +119,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment
mChannelOption = mSeriesRecording.getChannelOption();
mSelectedChannelId = Channel.INVALID_ID;
if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) {
- Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
+ Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId());
if (channel != null) {
mSelectedChannelId = channel.getId();
} else {
mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL;
}
}
- mChannelComparator = new Channel.DefaultComparator(context,
- TvApplication.getSingletons(context).getTvInputManagerHelper());
- mChannels.sort(mChannelComparator);
+ mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR);
mFragmentTitle = getString(R.string.dvr_series_settings_title);
mProrityActionTitle = getString(R.string.dvr_series_settings_priority);
mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest);
@@ -144,23 +136,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
@Override
+ public void onResume() {
+ super.onResume();
+ // To avoid the order of series's priority has changed, but series doesn't get update.
+ updatePriorityGuidedAction();
+ }
+
+ @Override
public void onDetach() {
super.onDetach();
mDvrDataManager.removeSeriesRecordingListener(this);
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
- mEpisodicProgramLoadTask = null;
- }
}
@Override
public void onDestroy() {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
- if (getFragmentManager().getBackStackEntryCount() == mBackStackCount
- && getArguments()
- .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)
- && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) {
- dvrManager.removeSeriesRecording(mSeriesRecordingId);
+ if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments()
+ .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) {
+ mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId);
}
super.onDestroy();
}
@@ -178,7 +170,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment
.id(ACTION_ID_PRIORITY)
.title(mProrityActionTitle)
.build();
- updatePriorityGuidedAction(false);
actions.add(mPriorityGuidedAction);
mChannelsGuidedAction = new GuidedAction.Builder(getActivity())
@@ -204,10 +195,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == GuidedAction.ACTION_ID_OK) {
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
- mEpisodicProgramLoadTask = null;
- }
if (mChannelOption != mSeriesRecording.getChannelOption()
|| mSeriesRecording.isStopped()
|| (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE
@@ -218,28 +205,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment
if (mSelectedChannelId != Channel.INVALID_ID) {
builder.setChannelId(mSelectedChannelId);
}
- TvApplication.getSingletons(getContext()).getDvrManager()
- .updateSeriesRecording(builder.build());
- SeriesRecordingScheduler scheduler =
- SeriesRecordingScheduler.getInstance(getContext());
- // Since dialog is used even after the fragment is closed, we should
- // use application context.
- ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString(
- R.string.dvr_series_schedules_progress_message_updating_programs));
- scheduler.addOnSeriesRecordingUpdatedListener(
- new OnSeriesRecordingUpdatedListener() {
- @Override
- public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) {
- for (SeriesRecording seriesRecording : seriesRecordings) {
- if (seriesRecording.getId() == mSeriesRecordingId) {
- dialog.dismiss();
- scheduler.removeOnSeriesRecordingUpdatedListener(this);
- showConfirmDialog();
- return;
- }
- }
- }
- });
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ dvrManager.updateSeriesRecording(builder.build());
+ if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL
+ || mSelectedChannelId == mCurrentProgram.getChannelId())) {
+ dvrManager.addSchedule(mCurrentProgram);
+ }
+ updateSchedulesToSeries();
+ showConfirmDialog();
} else {
showConfirmDialog();
}
@@ -247,9 +220,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment
finishGuidedStepFragments();
} else if (actionId == ACTION_ID_PRIORITY) {
FragmentManager fragmentManager = getFragmentManager();
- PrioritySettingsFragment fragment = new PrioritySettingsFragment();
+ DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment();
Bundle args = new Bundle();
- args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID,
+ args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID,
mSeriesRecording.getId());
fragment.setArguments(args);
GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame);
@@ -281,7 +254,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private void updateChannelsGuidedAction(boolean notifyActionChanged) {
if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) {
mChannelsGuidedAction.setDescription(mChannelsActionAllText);
- } else {
+ } else if (mId2Channel.get(mSelectedChannelId) != null){
mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId)
.getDisplayText());
}
@@ -290,7 +263,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
}
- private void updatePriorityGuidedAction(boolean notifyActionChanged) {
+ private void updatePriorityGuidedAction() {
int totalSeriesCount = 0;
int priorityOrder = 0;
for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) {
@@ -312,49 +285,38 @@ public class SeriesSettingsFragment extends GuidedStepFragment
mPriorityGuidedAction.setDescription(getString(
R.string.dvr_series_settings_priority_rank, priorityOrder + 1));
}
- if (notifyActionChanged) {
- notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
- }
+ notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
}
- private void collectChannelsInBackground() {
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
+ private void updateSchedulesToSeries() {
+ List<Program> recordingCandidates = new ArrayList<>();
+ Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>();
+ for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) {
+ if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
+ && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
+ scheduledEpisodes.add(new SeasonEpisodeNumber(
+ r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()));
+ }
}
- mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
- @Override
- protected void onPostExecute(List<Program> programs) {
- mEpisodicProgramLoadTask = null;
- Set<Long> channelIds = new HashSet<>();
- for (Program program : programs) {
- channelIds.add(program.getChannelId());
- }
- boolean channelAdded = false;
- for (Long channelId : channelIds) {
- if (mId2Channel.get(channelId) != null) {
- continue;
- }
- Channel channel = mChannelDataManager.getChannel(channelId);
- if (channel != null) {
- channelAdded = true;
- mId2Channel.put(channelId, channel);
- mChannels.add(channel);
- if (DEBUG) Log.d(TAG, "Added channel: " + channel);
- }
- }
- if (!channelAdded) {
- return;
- }
- mChannels.sort(mChannelComparator);
- mChannelsGuidedAction.setSubActions(buildChannelSubAction());
- notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL));
- if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask");
+ for (Program program : mPrograms) {
+ // Removes current programs and scheduled episodes out, matches the channel option.
+ if (program.getStartTimeUtcMillis() >= System.currentTimeMillis()
+ && mSeriesRecording.matchProgram(program)
+ && !scheduledEpisodes.contains(new SeasonEpisodeNumber(
+ mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) {
+ recordingCandidates.add(program);
}
- }.setLoadCurrentProgram(true)
- .setLoadDisallowedProgram(true)
- .setLoadScheduledEpisode(true)
- .setIgnoreChannelOption(true);
- mEpisodicProgramLoadTask.execute();
+ }
+ if (recordingCandidates.isEmpty()) {
+ return;
+ }
+ List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode(
+ mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates)
+ .get(mSeriesRecordingId);
+ if (!programsToSchedule.isEmpty()) {
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule);
+ }
}
private List<GuidedAction> buildChannelSubAction() {
@@ -373,8 +335,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
private void showConfirmDialog() {
- DvrUiHelper.StartSeriesScheduledDialogActivity(
- getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog);
+ DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording,
+ mShowViewScheduleOptionInDialog, mPrograms);
finishGuidedStepFragments();
}
@@ -382,16 +344,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
@Override
- public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { }
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (series.getId() == mSeriesRecording.getId()) {
+ finishGuidedStepFragments();
+ return;
+ }
+ }
+ }
@Override
public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
for (SeriesRecording seriesRecording : seriesRecordings) {
if (seriesRecording.getId() == mSeriesRecordingId) {
mSeriesRecording = seriesRecording;
- updatePriorityGuidedAction(true);
+ updatePriorityGuidedAction();
return;
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index c3867886..b476fff7 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -33,7 +33,7 @@ import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -131,15 +131,8 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
String title = getString(R.string.dvr_stop_recording_dialog_title);
String description;
if (mStopReason == REASON_ON_CONFLICT) {
- String programTitle = mSchedule.getProgramTitle();
- if (TextUtils.isEmpty(programTitle)) {
- ChannelDataManager channelDataManager =
- TvApplication.getSingletons(getActivity()).getChannelDataManager();
- Channel channel = channelDataManager.getChannel(mSchedule.getChannelId());
- programTitle = channel.getDisplayName();
- }
description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict,
- mSchedule.getProgramTitle());
+ mSchedule.getProgramDisplayTitle(getContext()));
} else {
description = getString(R.string.dvr_stop_recording_dialog_description);
}
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
index feaa2357..fe3a4a60 100644
--- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
@@ -31,8 +31,8 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index c0d3b0c5..507db6e7 100644
--- a/src/com/android/tv/dvr/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -14,19 +14,21 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.ProgressDialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.media.tv.TvInputManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
-import android.text.TextUtils;
import android.widget.ImageView;
import android.widget.Toast;
@@ -36,32 +38,36 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
-import com.android.tv.dvr.ui.DvrDetailsActivity;
-import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment;
+import com.android.tv.dialog.HalfSizedDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
-import com.android.tv.dvr.ui.DvrSchedulesActivity;
-import com.android.tv.dvr.ui.DvrSeriesDeletionActivity;
-import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity;
-import com.android.tv.dvr.ui.DvrSeriesSettingsActivity;
-import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment;
-import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
+import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/**
* A helper class for DVR UI.
@@ -69,94 +75,54 @@ import java.util.List;
@MainThread
@TargetApi(Build.VERSION_CODES.N)
public class DvrUiHelper {
- /**
- * Handles the action to create the new schedule. It returns {@code true} if the schedule is
- * added and there's no additional UI, otherwise {@code false}.
- */
- public static boolean handleCreateSchedule(MainActivity activity, Program program) {
- if (program == null) {
- return false;
- }
- DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
- if (!program.isEpisodic()) {
- // One time recording.
- dvrManager.addSchedule(program);
- if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
- DvrUiHelper.showScheduleConflictDialog(activity, program);
- return false;
- }
- } else {
- SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
- if (seriesRecording == null || seriesRecording.isStopped()) {
- DvrUiHelper.showScheduleDialog(activity, program);
- return false;
- } else {
- // Show recorded program rather than the schedule.
- RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(),
- program.getSeasonNumber(), program.getEpisodeNumber());
- if (recordedProgram != null) {
- DvrUiHelper.showAlreadyRecordedDialog(activity, program);
- return false;
- }
- ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(),
- program.getSeasonNumber(), program.getEpisodeNumber());
- if (duplicate != null
- && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
- || duplicate.getState()
- == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
- DvrUiHelper.showAlreadyScheduleDialog(activity, program);
- return false;
- }
- // Just add the schedule.
- dvrManager.addSchedule(program);
- }
- }
- return true;
+ private static String TAG = "DvrUiHelper";
- }
+ private static ProgressDialog sProgressDialog = null;
/**
* Checks if the storage status is good for recording and shows error messages if needed.
*
- * @return true if the storage status is fine to be recorded for {@code inputId}.
+ * @param recordingRequestRunnable if the storage status is OK to record or users choose to
+ * perform the operation anyway, this Runnable will run.
*/
- public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) {
- if (!Utils.isBundledInput(inputId)) {
- return true;
- }
- DvrStorageStatusManager dvrStorageStatusManager =
- TvApplication.getSingletons(activity).getDvrStorageStatusManager();
- int status = dvrStorageStatusManager.getDvrStorageStatus();
- if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
- showDvrSmallSizedStorageErrorDialog(activity);
- return false;
- } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
- showDvrMissingStorageErrorDialog(activity, inputId);
- return false;
- } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) {
- // TODO: handle insufficient storage case.
- return true;
- } else {
- return true;
+ public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId,
+ Runnable recordingRequestRunnable) {
+ if (Utils.isBundledInput(inputId)) {
+ switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager()
+ .getDvrStorageStatus()) {
+ case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL:
+ showDvrSmallSizedStorageErrorDialog(activity);
+ return;
+ case DvrStorageStatusManager.STORAGE_STATUS_MISSING:
+ showDvrMissingStorageErrorDialog(activity);
+ return;
+ case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT:
+ showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable);
+ return;
+ }
}
+ recordingRequestRunnable.run();
}
/**
* Shows the schedule dialog.
*/
- public static void showScheduleDialog(MainActivity activity, Program program) {
+ public static void showScheduleDialog(Activity activity, Program program,
+ boolean addCurrentProgramToSeries) {
if (SoftPreconditions.checkNotNull(program) == null) {
return;
}
Bundle args = new Bundle();
args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES,
+ addCurrentProgramToSeries);
showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true);
}
/**
* Shows the recording duration options dialog.
*/
- public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) {
+ public static void showChannelRecordDurationOptions(Activity activity, Channel channel) {
if (SoftPreconditions.checkNotNull(channel) == null) {
return;
}
@@ -168,7 +134,7 @@ public class DvrUiHelper {
/**
* Shows the dialog which says that the new schedule conflicts with others.
*/
- public static void showScheduleConflictDialog(MainActivity activity, Program program) {
+ public static void showScheduleConflictDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -192,20 +158,47 @@ public class DvrUiHelper {
/**
* Shows DVR insufficient space error dialog.
*/
- public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) {
- showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null);
+ public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity,
+ Set<String> failedScheduledRecordingInfoSet) {
+ Bundle args = new Bundle();
+ ArrayList<String> failedScheduledRecordingInfoArray =
+ new ArrayList<>(failedScheduledRecordingInfoSet);
+ args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS,
+ failedScheduledRecordingInfoArray);
+ showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args);
Utils.clearRecordingFailedReason(activity,
TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ Utils.clearFailedScheduledRecordingInfoSet(activity);
+ }
+
+ /**
+ * Shows DVR no free space error dialog.
+ *
+ * @param recordingRequestRunnable the recording request to be executed when users choose
+ * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}.
+ */
+ public static void showDvrNoFreeSpaceErrorDialog(Activity activity,
+ Runnable recordingRequestRunnable) {
+ DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment();
+ fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) {
+ recordingRequestRunnable.run();
+ } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) {
+ Intent intent = new Intent(activity, DvrBrowseActivity.class);
+ activity.startActivity(intent);
+ }
+ }
+ });
+ showDialogFragment(activity, fragment, null);
}
/**
* Shows DVR missing storage error dialog.
*/
- private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) {
- SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId));
- Bundle args = new Bundle();
- args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId);
- showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args);
+ private static void showDvrMissingStorageErrorDialog(Activity activity) {
+ showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null);
}
/**
@@ -231,7 +224,7 @@ public class DvrUiHelper {
/**
* Shows "already scheduled" dialog.
*/
- public static void showAlreadyScheduleDialog(MainActivity activity, Program program) {
+ public static void showAlreadyScheduleDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -243,7 +236,7 @@ public class DvrUiHelper {
/**
* Shows "already recorded" dialog.
*/
- public static void showAlreadyRecordedDialog(MainActivity activity, Program program) {
+ public static void showAlreadyRecordedDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -252,6 +245,87 @@ public class DvrUiHelper {
showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true);
}
+ /**
+ * Handle the request of recording a current program. It will handle creating schedules and
+ * shows the proper dialog and toast message respectively for timed-recording and program
+ * recording cases.
+ *
+ * @param addProgramToSeries denotes whether the program to be recorded should be added into
+ * the series recording when users choose to record the entire series.
+ */
+ public static void requestRecordingCurrentProgram(Activity activity,
+ Channel channel, Program program, boolean addProgramToSeries) {
+ if (program == null) {
+ DvrUiHelper.showChannelRecordDurationOptions(activity, channel);
+ } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
+ String msg = activity.getString(R.string.dvr_msg_current_program_scheduled,
+ program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false));
+ Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Handle the request of recording a future program. It will handle creating schedules and
+ * shows the proper toast message.
+ *
+ * @param addProgramToSeries denotes whether the program to be recorded should be added into
+ * the series recording when users choose to record the entire series.
+ */
+ public static void requestRecordingFutureProgram(Activity activity,
+ Program program, boolean addProgramToSeries) {
+ if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
+ String msg = activity.getString(
+ R.string.dvr_msg_program_scheduled, program.getTitle());
+ ToastUtils.show(activity, msg, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /**
+ * Handles the action to create the new schedule. It returns {@code true} if the schedule is
+ * added and there's no additional UI, otherwise {@code false}.
+ */
+ private static boolean handleCreateSchedule(Activity activity, Program program,
+ boolean addProgramToSeries) {
+ if (program == null) {
+ return false;
+ }
+ DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
+ if (!program.isEpisodic()) {
+ // One time recording.
+ dvrManager.addSchedule(program);
+ if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
+ DvrUiHelper.showScheduleConflictDialog(activity, program);
+ return false;
+ }
+ } else {
+ // Show recorded program rather than the schedule.
+ RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (recordedProgram != null) {
+ DvrUiHelper.showAlreadyRecordedDialog(activity, program);
+ return false;
+ }
+ ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (duplicate != null
+ && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || duplicate.getState()
+ == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ DvrUiHelper.showAlreadyScheduleDialog(activity, program);
+ return false;
+ }
+ SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
+ if (seriesRecording == null || seriesRecording.isStopped()) {
+ DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries);
+ return false;
+ } else {
+ // Just add the schedule.
+ dvrManager.addSchedule(program);
+ }
+ }
+ return true;
+ }
+
private static void showDialogFragment(Activity activity,
DvrHalfSizedDialogFragment dialogFragment, Bundle args) {
showDialogFragment(activity, dialogFragment, args, false, false);
@@ -341,19 +415,66 @@ public class DvrUiHelper {
/**
* Shows the series settings activity.
*
- * @param channelIds Channel ID list which has programs belonging to the series.
+ * @param programs list of programs which belong to the series.
*/
public static void startSeriesSettingsActivity(Context context, long seriesRecordingId,
- @Nullable long[] channelIds, boolean removeEmptySeriesSchedule,
- boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) {
+ @Nullable List<Program> programs, boolean removeEmptySeriesSchedule,
+ boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog,
+ Program currentProgram) {
+ SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager()
+ .getSeriesRecording(seriesRecordingId);
+ if (series == null) {
+ return;
+ }
+ if (programs != null) {
+ startSeriesSettingsActivityInternal(context, seriesRecordingId, programs,
+ removeEmptySeriesSchedule, isWindowTranslucent,
+ showViewScheduleOptionInDialog, currentProgram);
+ } else {
+ EpisodicProgramLoadTask episodicProgramLoadTask =
+ new EpisodicProgramLoadTask(context, series) {
+ @Override
+ protected void onPostExecute(List<Program> loadedPrograms) {
+ sProgressDialog.dismiss();
+ sProgressDialog = null;
+ startSeriesSettingsActivityInternal(context, seriesRecordingId,
+ loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms,
+ removeEmptySeriesSchedule, isWindowTranslucent,
+ showViewScheduleOptionInDialog, currentProgram);
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true);
+ sProgressDialog = ProgressDialog.show(context, null, context.getString(
+ R.string.dvr_series_progress_message_reading_programs), true, true,
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ episodicProgramLoadTask.cancel(true);
+ sProgressDialog = null;
+ }
+ });
+ episodicProgramLoadTask.execute();
+ }
+ }
+
+ private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId,
+ @NonNull List<Program> programs, boolean removeEmptySeriesSchedule,
+ boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog,
+ Program currentProgram) {
+ SoftPreconditions.checkState(programs != null,
+ TAG, "Start series settings activity but programs is null");
Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId);
- intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs);
intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING,
removeEmptySeriesSchedule);
intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent);
intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG,
showViewScheduleOptionInDialog);
+ intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram);
context.startActivity(intent);
}
@@ -361,7 +482,8 @@ public class DvrUiHelper {
* Shows "series recording scheduled" dialog activity.
*/
public static void StartSeriesScheduledDialogActivity(Context context,
- SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) {
+ SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog,
+ List<Program> programs) {
if (seriesRecording == null) {
return;
}
@@ -370,6 +492,9 @@ public class DvrUiHelper {
seriesRecording.getId());
intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION,
showViewScheduleOptionInDialog);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS,
+ programs);
context.startActivity(intent);
}
@@ -447,4 +572,4 @@ public class DvrUiHelper {
Utils.toTimeString(endTimeMs, false));
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java
new file mode 100644
index 00000000..4f06ebcf
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/FadeBackground.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+
+/**
+ * This transition fades in/out of the background of the view by changing the background color.
+ */
+public class FadeBackground extends Transition {
+ private final int mMode;
+
+ public FadeBackground(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadeBackground);
+ mMode = a.getInt(R.styleable.FadeBackground_fadingMode, Visibility.MODE_IN);
+ a.recycle();
+ }
+
+ @Override
+ public void captureStartValues(TransitionValues transitionValues) { }
+
+ @Override
+ public void captureEndValues(TransitionValues transitionValues) { }
+
+ @Override
+ public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ Drawable background = endValues.view.getBackground();
+ if (background instanceof ColorDrawable) {
+ int color = ((ColorDrawable) background).getColor();
+ int transparentColor = Color.argb(0, Color.red(color), Color.green(color),
+ Color.blue(color));
+ return mMode == Visibility.MODE_OUT
+ ? ObjectAnimator.ofArgb(background, "color", transparentColor)
+ : ObjectAnimator.ofArgb(background, "color", transparentColor, color);
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
index 393a5ff3..8c0af9ed 100644
--- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -20,11 +20,15 @@ import android.support.annotation.VisibleForTesting;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.PresenterSelector;
+import com.android.tv.common.SoftPreconditions;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Keeps a set of items sorted
@@ -35,16 +39,18 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
private final Comparator<T> mComparator;
private final int mMaxItemCount;
private int mExtraItemCount;
+ private final Set<Long> mIds = new HashSet<>();
- SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
+ public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
this(presenterSelector, comparator, Integer.MAX_VALUE);
}
- SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator,
+ public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator,
int maxItemCount) {
super(presenterSelector);
mComparator = comparator;
mMaxItemCount = maxItemCount;
+ setHasStableIds(true);
}
/**
@@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
final void setInitialItems(List<T> items) {
List<T> itemsCopy = new ArrayList<>(items);
Collections.sort(itemsCopy, mComparator);
- addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size())));
+ for (T item : itemsCopy) {
+ add(item, true);
+ if (size() == mMaxItemCount) {
+ break;
+ }
+ }
}
/**
@@ -82,6 +93,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
* the end to save search time.
*/
public final void add(T item, boolean insertToEnd) {
+ long newItemId = getId(item);
+ SoftPreconditions.checkState(!mIds.contains(newItemId));
+ mIds.add(newItemId);
int i;
if (insertToEnd) {
i = findInsertPosition(item);
@@ -89,8 +103,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
i = findInsertPositionBinary(item);
}
super.add(i, item);
- if (size() > mMaxItemCount + mExtraItemCount) {
- removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount);
+ if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) {
+ Object removedItem = get(mMaxItemCount);
+ remove(removedItem);
}
}
@@ -100,48 +115,97 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
* They will be presented in their insertion order.
*/
public int addExtraItem(T item) {
+ long newItemId = getId(item);
+ SoftPreconditions.checkState(!mIds.contains(newItemId));
+ mIds.add(newItemId);
super.add(item);
return ++mExtraItemCount;
}
+ @Override
+ public boolean remove(Object item) {
+ return removeWithId((T) item);
+ }
+
/**
* Removes an item which has the same ID as {@code item}.
*/
public boolean removeWithId(T item) {
- int index = indexWithTypeAndId(item);
- return index >= 0 && index < size() && remove(get(index));
+ int index = indexWithId(item);
+ return index >= 0 && index < size() && removeItems(index, 1) == 1;
+ }
+
+ @Override
+ public int removeItems(int position, int count) {
+ int upperBound = Math.min(position + count, size());
+ for (int i = position; i < upperBound; i++) {
+ mIds.remove(getId((T) get(i)));
+ }
+ if (upperBound > size() - mExtraItemCount) {
+ mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position);
+ }
+ return super.removeItems(position, count);
+ }
+
+ @Override
+ public void replace(int position, Object item) {
+ boolean wasExtra = position >= size() - mExtraItemCount;
+ removeItems(position, 1);
+ if (!wasExtra) {
+ add(item);
+ } else {
+ addExtraItem((T) item);
+ }
+ }
+
+ @Override
+ public void clear() {
+ mIds.clear();
+ super.clear();
}
/**
- * Change an item in the list.
+ * Changes an item in the list.
* @param item The item to change.
*/
public final void change(T item) {
- int oldIndex = indexWithTypeAndId(item);
+ int oldIndex = indexWithId(item);
if (oldIndex != -1) {
T old = (T) get(oldIndex);
if (mComparator.compare(old, item) == 0) {
replace(oldIndex, item);
return;
}
- removeItems(oldIndex, 1);
+ remove(old);
}
add(item);
}
/**
+ * Checks whether the item is in the list.
+ */
+ public final boolean contains(T item) {
+ return indexWithId(item) != -1;
+ }
+
+ @Override
+ public long getId(int position) {
+ return getId((T) get(position));
+ }
+
+ /**
* Returns the id of the the given {@code item}, which will be used in {@link #change} to
* decide if the given item is already existed in the adapter.
*
* The id must be stable.
*/
- abstract long getId(T item);
+ protected abstract long getId(T item);
- private int indexWithTypeAndId(T item) {
+ private int indexWithId(T item) {
long id = getId(item);
for (int i = 0; i < size() - mExtraItemCount; i++) {
T r = (T) get(i);
- if (r.getClass() == item.getClass() && getId(r) == id) {
+ if (getId(r) == id) {
return i;
}
}
diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index 8b8cd5c5..38a78f5d 100644
--- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.R;
@@ -110,11 +110,7 @@ class ActionPresenterSelector extends PresenterSelector {
.getDimensionPixelSize(R.dimen.lb_action_padding_horizontal);
vh.view.setPaddingRelative(padding, 0, padding, 0);
}
- if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) {
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null);
- } else {
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
- }
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
CharSequence line1 = action.getLabel1();
CharSequence line2 = action.getLabel2();
@@ -130,7 +126,7 @@ class ActionPresenterSelector extends PresenterSelector {
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
ActionViewHolder vh = (ActionViewHolder) viewHolder;
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null);
vh.view.setPadding(0, 0, 0, 0);
vh.mAction = null;
}
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
new file mode 100644
index 00000000..c8f6a03f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dialog.HalfSizedDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
+
+/**
+ * {@link RecordingDetailsFragment} for current recording in DVR.
+ */
+public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
+ private static final int ACTION_STOP_RECORDING = 1;
+
+ private DvrDataManager mDvrDataManger;
+ private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
+ new DvrDataManager.ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) { }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == getRecording().getId()) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == getRecording().getId()
+ && schedule.getState()
+ != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING,
+ res.getString(R.string.epg_dvr_dialog_message_stop_recording), null,
+ res.getDrawable(R.drawable.lb_ic_stop)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_STOP_RECORDING) {
+ DvrUiHelper.showStopRecordingDialog(getActivity(),
+ getRecording().getChannelId(),
+ DvrStopRecordingFragment.REASON_USER_STOP,
+ new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
+ DvrManager dvrManager =
+ TvApplication.getSingletons(getContext())
+ .getDvrManager();
+ dvrManager.stopRecording(getRecording());
+ getActivity().finish();
+ }
+ }
+ });
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onDetach() {
+ if (mDvrDataManger != null) {
+ mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener);
+ }
+ super.onDetach();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index 19521fca..b43d1f12 100644
--- a/src/com/android/tv/dvr/ui/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.media.tv.TvContract;
import android.support.annotation.Nullable;
@@ -26,7 +26,7 @@ import com.android.tv.data.Channel;
/**
* A class for details content.
*/
-public class DetailsContent {
+class DetailsContent {
/** Constant for invalid time. */
public static final long INVALID_TIME = -1;
diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
index 175f05bc..a2e3fe16 100644
--- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.animation.Animator;
@@ -38,13 +38,14 @@ import com.android.tv.util.Utils;
/**
* An {@link Presenter} for rendering a detailed description of an DVR item.
- * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}.
+ * Typically this Presenter will be used in a
+ * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}.
* Most codes of this class is originated from
* {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
* The latter class are re-used to provide a customized version of
* {@link android.support.v17.leanback.widget.DetailsOverviewRow}.
*/
-public class DetailsContentPresenter extends Presenter {
+class DetailsContentPresenter extends Presenter {
/**
* The ViewHolder for the {@link DetailsContentPresenter}.
*/
@@ -113,6 +114,20 @@ public class DetailsContentPresenter extends Presenter {
public ViewHolder(final View view) {
super(view);
+ view.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // In case predraw listener was removed in detach, make sure
+ // we have the proper layout.
+ addPreDrawListener();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removePreDrawListener();
+ }
+ });
mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
@@ -276,22 +291,6 @@ public class DetailsContentPresenter extends Presenter {
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { }
- @Override
- public void onViewAttachedToWindow(Presenter.ViewHolder holder) {
- // In case predraw listener was removed in detach, make sure
- // we have the proper layout.
- ViewHolder vh = (ViewHolder) holder;
- vh.addPreDrawListener();
- super.onViewAttachedToWindow(holder);
- }
-
- @Override
- public void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
- ViewHolder vh = (ViewHolder) holder;
- vh.removePreDrawListener();
- super.onViewDetachedFromWindow(holder);
- }
-
private void setTopMargin(View view, int topMargin) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
lp.topMargin = topMargin;
diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 6714ecd3..82fe9ce3 100644
--- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.graphics.drawable.BitmapDrawable;
@@ -26,7 +26,7 @@ import android.support.v17.leanback.app.BackgroundManager;
/**
* The Background Helper.
*/
-public class DetailsViewBackgroundHelper {
+class DetailsViewBackgroundHelper {
// Background delay serves to avoid kicking off expensive bitmap loading
// in case multiple backgrounds are set in quick succession.
private static final int SET_BACKGROUND_DELAY_MS = 100;
diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 45fb1cf1..2b3dcb25 100644
--- a/src/com/android/tv/dvr/ui/DvrActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.os.Bundle;
@@ -25,11 +25,11 @@ import com.android.tv.TvApplication;
/**
* {@link android.app.Activity} for DVR UI.
*/
-public class DvrActivity extends Activity {
+public class DvrBrowseActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.dvr_main);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index a6dd31d1..803d1017 100644
--- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -14,10 +14,9 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
-import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.app.BrowseFragment;
@@ -25,11 +24,11 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.TitleViewAdapter;
-import android.text.TextUtils;
import android.util.Log;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
@@ -42,9 +41,10 @@ import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -79,6 +79,20 @@ public class DvrBrowseFragment extends BrowseFragment implements
private ClassPresenterSelector mPresenterSelector;
private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
private final Handler mHandler = new Handler();
+ private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+ new OnGlobalFocusChangeListener() {
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (oldFocus instanceof RecordingCardView) {
+ ((RecordingCardView) oldFocus).expandTitle(false, true);
+ }
+ if (newFocus instanceof RecordingCardView) {
+ // If the header transition is ongoing, expand cards immediately without
+ // animation to make a smooth transition.
+ ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition());
+ }
+ }
+ };
private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() {
@Override
@@ -128,7 +142,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) {
if (mScheduleAdapter != null) {
for (ScheduledRecording schedule : schedules) {
- onScheduledRecordingStatusChanged(schedule);
+ onScheduledRecordingConflictStatusChanged(schedule);
}
}
}
@@ -154,16 +168,12 @@ public class DvrBrowseFragment extends BrowseFragment implements
new ScheduledRecordingPresenter(context))
.addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context))
.addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context))
- .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter());
+ .addClassPresenter(FullScheduleCardHolder.class,
+ new FullSchedulesCardPresenter(context));
mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
mGenreLabels.add(getString(R.string.dvr_main_others));
- setupUiElements();
- setupAdapters();
- mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
- prepareEntranceTransition();
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- } else {
+ prepareUiElements();
+ if (!startBrowseIfDvrInitialized()) {
if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
}
@@ -174,6 +184,19 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
@Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ }
+
+ @Override
+ public void onDestroyView() {
+ getView().getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ super.onDestroyView();
+ }
+
+ @Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
mHandler.removeCallbacks(mUpdateRowsRunnable);
@@ -195,25 +218,13 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public void onDvrScheduleLoadFinished() {
- List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings();
- onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings));
- List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings();
- onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings));
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- }
+ startBrowseIfDvrInitialized();
mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
}
@Override
public void onRecordedProgramLoadFinished() {
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, true);
- }
- updateRows();
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- }
+ startBrowseIfDvrInitialized();
mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
}
@@ -270,6 +281,18 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
}
+ private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (needToShowScheduledRecording(schedule)) {
+ if (mScheduleAdapter.contains(schedule)) {
+ mScheduleAdapter.change(schedule);
+ }
+ } else {
+ mScheduleAdapter.removeWithId(schedule);
+ }
+ }
+ }
+
@Override
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
@@ -295,44 +318,53 @@ public class DvrBrowseFragment extends BrowseFragment implements
super.showTitle(flags);
}
- private void setupUiElements() {
+ private void prepareUiElements() {
setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(false);
setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
+ mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext()));
+ setAdapter(mRowsAdapter);
+ prepareEntranceTransition();
}
- private void setupAdapters() {
- mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
- mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
- mSeriesAdapter = new SeriesAdapter();
- for (int i = 0; i < mGenreAdapters.length; i++) {
- mGenreAdapters[i] = new RecordedProgramAdapter();
- }
- // Schedule Recordings.
- List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
- onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
- mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
- // Recorded Programs.
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, false);
- }
- // Series Recordings. Series recordings should be added after recorded programs, because
- // we build series recordings' latest program information while adding recorded programs.
- List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
- handleSeriesRecordingsAdded(recordings);
- mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
- mRecentRow = new ListRow(new HeaderItem(
- getString(R.string.dvr_main_recent)), mRecentAdapter);
- mRowsAdapter.add(new ListRow(new HeaderItem(
- getString(R.string.dvr_main_scheduled)), mScheduleAdapter));
- mSeriesRow = new ListRow(new HeaderItem(
- getString(R.string.dvr_main_series)), mSeriesAdapter);
- updateRows();
- mDvrDataManager.addRecordedProgramListener(this);
- mDvrDataManager.addScheduledRecordingListener(this);
- mDvrDataManager.addSeriesRecordingListener(this);
- setAdapter(mRowsAdapter);
+ private boolean startBrowseIfDvrInitialized() {
+ if (mDvrDataManager.isInitialized()) {
+ // Setup rows
+ mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
+ mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
+ mSeriesAdapter = new SeriesAdapter();
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ mGenreAdapters[i] = new RecordedProgramAdapter();
+ }
+ // Schedule Recordings.
+ List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
+ onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
+ mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
+ // Recorded Programs.
+ for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
+ handleRecordedProgramAdded(recordedProgram, false);
+ }
+ // Series Recordings. Series recordings should be added after recorded programs, because
+ // we build series recordings' latest program information while adding recorded programs.
+ List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
+ handleSeriesRecordingsAdded(recordings);
+ mRecentRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_recent)), mRecentAdapter);
+ mRowsAdapter.add(new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_scheduled)), mScheduleAdapter));
+ mSeriesRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_series)), mSeriesAdapter);
+ updateRows();
+ // Initialize listeners
+ mDvrDataManager.addRecordedProgramListener(this);
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
+ startEntranceTransition();
+ return true;
+ }
+ return false;
}
private void handleRecordedProgramAdded(RecordedProgram recordedProgram,
@@ -589,10 +621,11 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public long getId(Object item) {
+ // We takes the inverse number for the ID of recorded programs to make the ID stable.
if (item instanceof SeriesRecording) {
return ((SeriesRecording) item).getId();
} else if (item instanceof RecordedProgram) {
- return ((RecordedProgram) item).getId();
+ return -((RecordedProgram) item).getId() - 1;
} else {
return -1;
}
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
index 806c775c..30c81e83 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.os.Bundle;
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 21f9c4b4..4d3698ef 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
import android.content.Intent;
@@ -48,8 +48,8 @@ import com.android.tv.data.BaseProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dialog.PinDialogFragment;
-import com.android.tv.dvr.DvrPlaybackActivity;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.ToastUtils;
diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
index 339e5d2f..317b6af3 100644
--- a/src/com/android/tv/dvr/ui/DvrItemPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.support.annotation.CallSuper;
@@ -22,16 +22,17 @@ import android.support.v17.leanback.widget.Presenter;
import android.view.View;
import android.view.View.OnClickListener;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ui.DvrUiHelper;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.Set;
/**
* An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in
- * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording},
- * {@link RecordedProgram}, and {@link SeriesRecording}.
+ * {@link DvrBrowseFragment}. DVR items might include:
+ * {@link com.android.tv.dvr.data.ScheduledRecording},
+ * {@link com.android.tv.dvr.data.RecordedProgram}, and
+ * {@link com.android.tv.dvr.data.SeriesRecording}.
*/
public abstract class DvrItemPresenter extends Presenter {
private final Set<ViewHolder> mBoundViewHolders = new HashSet<>();
@@ -49,6 +50,8 @@ public abstract class DvrItemPresenter extends Presenter {
@CallSuper
public void onUnbindViewHolder(ViewHolder viewHolder) {
mBoundViewHolders.remove(viewHolder);
+ viewHolder.view.setTag(null);
+ viewHolder.view.setOnClickListener(null);
}
/**
diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
new file mode 100644
index 00000000..37a72eaf
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+
+/** A list row presenter to display expand/fold card views list. */
+public class DvrListRowPresenter extends ListRowPresenter {
+ public DvrListRowPresenter(Context context) {
+ super();
+ setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ setExpandedRowHeight(
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height));
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
index d4d4d8ab..311137a9 100644
--- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java
+++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
/**
* Special object for schedule preview;
diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
index 7dd85f45..6d4763d4 100644
--- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
-import android.support.v17.leanback.widget.Presenter;
+import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.Utils;
import java.util.Collections;
@@ -33,23 +33,31 @@ import java.util.List;
/**
* Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
*/
-public class FullSchedulesCardPresenter extends Presenter {
+class FullSchedulesCardPresenter extends DvrItemPresenter {
+ private Context mContext;
+ private final Drawable mIconDrawable;
+ private final String mCardTitleText;
+
+ public FullSchedulesCardPresenter(Context context) {
+ mContext = context;
+ mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule);
+ mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title);
+ }
+
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
Context context = parent.getContext();
RecordingCardView view = new RecordingCardView(context);
- return new ScheduledRecordingViewHolder(view);
+ return new ViewHolder(view);
}
@Override
- public void onBindViewHolder(ViewHolder baseHolder, Object o) {
- final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- final Context context = viewHolder.view.getContext();
+ public void onBindViewHolder(ViewHolder vh, Object o) {
+ final RecordingCardView cardView = (RecordingCardView) vh.view;
- cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule));
- cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title));
- List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context)
+ cardView.setImage(mIconDrawable);
+ cardView.setTitle(mCardTitleText);
+ List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext)
.getDvrDataManager().getAvailableScheduledRecordings();
int fullDays = 0;
if (!scheduledRecordings.isEmpty()) {
@@ -57,28 +65,24 @@ public class FullSchedulesCardPresenter extends Presenter {
Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR)
.getStartTimeMs()) + 1;
}
- cardView.setContent(context.getResources().getQuantityString(
+ cardView.setContent(mContext.getResources().getQuantityString(
R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null);
-
- View.OnClickListener clickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- DvrUiHelper.startSchedulesActivity(context, null);
- }
- };
- baseHolder.view.setOnClickListener(clickListener);
+ super.onBindViewHolder(vh, o);
}
@Override
- public void onUnbindViewHolder(ViewHolder baseHolder) {
- ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- cardView.reset();
+ public void onUnbindViewHolder(ViewHolder vh) {
+ ((RecordingCardView) vh.view).reset();
+ super.onUnbindViewHolder(vh);
}
- private static final class ScheduledRecordingViewHolder extends ViewHolder {
- ScheduledRecordingViewHolder(RecordingCardView view) {
- super(view);
- }
+ @Override
+ protected View.OnClickListener onCreateOnClickListener() {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DvrUiHelper.startSchedulesActivity(mContext, null);
+ }
+ };
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index e698b8a2..fe9b9de5 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.media.tv.TvInputManager;
@@ -30,10 +30,10 @@ import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
/**
- * {@link DetailsFragment} for recorded program in DVR.
+ * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR.
*/
public class RecordedProgramDetailsFragment extends DvrDetailsFragment
implements DvrDataManager.RecordedProgramListener {
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
index 1bf34310..ee978797 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
@@ -14,31 +14,26 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
-import android.app.Activity;
import android.content.Context;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
-import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
-import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
+import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.util.Utils;
-import java.util.concurrent.TimeUnit;
-
/**
* Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}.
*/
@@ -50,6 +45,7 @@ public class RecordedProgramPresenter extends DvrItemPresenter {
private String mYesterdayString;
private final int mProgressBarColor;
private final boolean mShowEpisodeTitle;
+ private final boolean mExpandTitleWhenFocused;
private static final class RecordedProgramViewHolder extends ViewHolder
implements WatchedPositionChangedListener {
@@ -79,25 +75,27 @@ public class RecordedProgramPresenter extends DvrItemPresenter {
}
}
- public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) {
+ public RecordedProgramPresenter(Context context, boolean showEpisodeTitle,
+ boolean expandTitleWhenFocused) {
mContext = context;
- mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
- mTodayString = context.getString(R.string.dvr_date_today);
- mYesterdayString = context.getString(R.string.dvr_date_yesterday);
+ mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager();
+ mTodayString = mContext.getString(R.string.dvr_date_today);
+ mYesterdayString = mContext.getString(R.string.dvr_date_yesterday);
mDvrWatchedPositionManager =
- TvApplication.getSingletons(context).getDvrWatchedPositionManager();
- mProgressBarColor = context.getResources()
+ TvApplication.getSingletons(mContext).getDvrWatchedPositionManager();
+ mProgressBarColor = mContext.getResources()
.getColor(R.color.play_controls_progress_bar_watched);
mShowEpisodeTitle = showEpisodeTitle;
+ mExpandTitleWhenFocused = expandTitleWhenFocused;
}
public RecordedProgramPresenter(Context context) {
- this(context, false);
+ this(context, false, false);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
- RecordingCardView view = new RecordingCardView(mContext);
+ RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused);
return new RecordedProgramViewHolder(view, mProgressBarColor);
}
@@ -132,8 +130,7 @@ public class RecordedProgramPresenter extends DvrItemPresenter {
isChannelLogo = true;
}
cardView.setImageUri(imageUri, isChannelLogo);
- int durationMinutes =
- Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis()));
+ int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis()));
String durationString = getContext().getResources().getQuantityString(
R.plurals.dvr_program_duration, durationMinutes, durationMinutes);
cardView.setContent(getDescription(program), durationString);
diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index 51c3b03b..7b0a8cb9 100644
--- a/src/com/android/tv/dvr/ui/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -14,51 +14,69 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.BaseCardView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.ViewUtils;
import com.android.tv.util.ImageLoader;
/**
- * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or
- * {@link RecordedProgram} or
- * {@link com.android.tv.dvr.SeriesRecording}.
+ * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording}
+ * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
*/
-class RecordingCardView extends BaseCardView {
+public class RecordingCardView extends BaseCardView {
+ // This value should be the same with
+ // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
+ private final static int ANIMATION_DURATION = 150;
private final ImageView mImageView;
private final int mImageWidth;
private final int mImageHeight;
private String mImageUri;
- private final TextView mTitleView;
private final TextView mMajorContentView;
private final TextView mMinorContentView;
private final ProgressBar mProgressBar;
private final View mAffiliatedIconContainer;
private final ImageView mAffiliatedIcon;
private final Drawable mDefaultImage;
+ private final FrameLayout mTitleArea;
+ private final TextView mFoldedTitleView;
+ private final TextView mExpandedTitleView;
+ private final ValueAnimator mExpandTitleAnimator;
+ private final int mFoldedTitleHeight;
+ private final int mExpandedTitleHeight;
+ private final boolean mExpandTitleWhenFocused;
+ private boolean mExpanded;
- RecordingCardView(Context context) {
- this(context,
- context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width),
- context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height));
+ public RecordingCardView(Context context) {
+ this(context, false);
}
- RecordingCardView(Context context, int imageWidth, int imageHeight) {
+ public RecordingCardView(Context context, boolean expandTitleWhenFocused) {
+ this(context, context.getResources().getDimensionPixelSize(
+ R.dimen.dvr_library_card_image_layout_width), context.getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height),
+ expandTitleWhenFocused);
+ }
+
+ public RecordingCardView(Context context, int imageWidth, int imageHeight,
+ boolean expandTitleWhenFocused) {
super(context);
//TODO(dvr): move these to the layout XML.
setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
@@ -75,13 +93,73 @@ class RecordingCardView extends BaseCardView {
mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
- mTitleView = (TextView) findViewById(R.id.title);
mMajorContentView = (TextView) findViewById(R.id.content_major);
mMinorContentView = (TextView) findViewById(R.id.content_minor);
+ mTitleArea = (FrameLayout) findViewById(R.id.title_area);
+ mFoldedTitleView = (TextView) findViewById(R.id.title_one_line);
+ mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines);
+ mFoldedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height);
+ mExpandedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height);
+ mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION);
+ mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float value = (Float) valueAnimator.getAnimatedValue();
+ mExpandedTitleView.setAlpha(value);
+ mFoldedTitleView.setAlpha(1.0f - value);
+ ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight
+ + (mExpandedTitleHeight - mFoldedTitleHeight) * value));
+ }
+ });
+ mExpandTitleWhenFocused = expandTitleWhenFocused;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ if (mExpandTitleWhenFocused) {
+ if (gainFocus) {
+ expandTitle(true, true);
+ } else {
+ expandTitle(false, true);
+ }
+ }
+ }
+
+ /**
+ * Expands/folds the title area to show program title with two/one lines.
+ *
+ * @param expand {@code true} to expand the title area, or {@code false} to fold it.
+ * @param withAnimation {@code true} to expand/fold with animation.
+ */
+ public void expandTitle(boolean expand, boolean withAnimation) {
+ if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) {
+ if (withAnimation) {
+ if (expand) {
+ mExpandTitleAnimator.start();
+ } else {
+ mExpandTitleAnimator.reverse();
+ }
+ } else {
+ if (expand) {
+ mFoldedTitleView.setAlpha(0.0f);
+ mExpandedTitleView.setAlpha(1.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight);
+ } else {
+ mFoldedTitleView.setAlpha(1.0f);
+ mExpandedTitleView.setAlpha(0.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight);
+ }
+ }
+ mExpanded = expand;
+ }
}
void setTitle(CharSequence title) {
- mTitleView.setText(title);
+ mFoldedTitleView.setText(title);
+ mExpandedTitleView.setText(title);
}
void setContent(CharSequence majorContent, CharSequence minorContent) {
@@ -178,8 +256,9 @@ class RecordingCardView extends BaseCardView {
}
public void reset() {
- mTitleView.setText(null);
+ mFoldedTitleView.setText(null);
+ mExpandedTitleView.setText(null);
setContent(null, null);
mImageView.setImageDrawable(mDefaultImage);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index 4e19ec3f..a877e05f 100644
--- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
@@ -26,7 +26,7 @@ import android.text.style.TextAppearanceSpan;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
/**
* {@link DetailsFragment} for recordings in DVR.
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 60816bb5..eb0f4f0d 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.os.Bundle;
@@ -26,7 +26,7 @@ import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ui.DvrUiHelper;
/**
* {@link RecordingDetailsFragment} for scheduled recording in DVR.
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index 5f447f13..efc8785a 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
-import android.app.Activity;
import android.content.Context;
import android.media.tv.TvContract;
import android.os.Handler;
@@ -32,7 +31,7 @@ import com.android.tv.TvApplication;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Utils;
import java.util.concurrent.TimeUnit;
@@ -40,7 +39,7 @@ import java.util.concurrent.TimeUnit;
/**
* Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
*/
-public class ScheduledRecordingPresenter extends DvrItemPresenter {
+class ScheduledRecordingPresenter extends DvrItemPresenter {
private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
private final ChannelDataManager mChannelDataManager;
@@ -92,18 +91,17 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter {
}
public ScheduledRecordingPresenter(Context context) {
- ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mContext = context;
+ ApplicationSingletons singletons = TvApplication.getSingletons(mContext);
mChannelDataManager = singletons.getChannelDataManager();
mDvrManager = singletons.getDvrManager();
- mContext = context;
- mProgressBarColor = context.getResources()
+ mProgressBarColor = mContext.getResources()
.getColor(R.color.play_controls_recording_icon_color_on_focus);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
- Context context = parent.getContext();
- RecordingCardView view = new RecordingCardView(context);
+ RecordingCardView view = new RecordingCardView(mContext);
return new ScheduledRecordingViewHolder(view, mProgressBarColor);
}
@@ -144,9 +142,8 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter {
public void onUnbindViewHolder(ViewHolder baseHolder) {
ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
viewHolder.stopUpdateProgressBar();
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
viewHolder.mScheduledRecording = null;
- cardView.reset();
+ ((RecordingCardView) viewHolder.view).reset();
super.onUnbindViewHolder(viewHolder);
}
@@ -174,4 +171,4 @@ public class ScheduledRecordingPresenter extends DvrItemPresenter {
cardView.setTitle(title);
cardView.setImageUri(imageUri, isChannelLogo);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index e9e391d4..f7b60b50 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
@@ -28,7 +28,6 @@ import android.support.v17.leanback.widget.DetailsOverviewRow;
import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.PresenterSelector;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
@@ -39,12 +38,11 @@ import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
import com.android.tv.dvr.DvrWatchedPositionManager;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
import java.util.Collections;
import java.util.Comparator;
@@ -85,7 +83,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
mWatchLabel = getString(R.string.dvr_detail_watch);
mResumeLabel = getString(R.string.dvr_detail_series_resume);
mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null);
- mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true);
+ mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true);
super.onCreate(savedInstanceState);
}
@@ -158,7 +156,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
DetailsOverviewRowPresenter rowPresenter) {
ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
- presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());
+ presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
return presenterSelector;
}
@@ -203,10 +201,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
mDvrDataManager.removeSeriesRecordingListener(this);
mDvrDataManager.removeRecordedProgramListener(this);
if (mSeries != null) {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
- if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) {
- dvrManager.removeSeriesRecording(mSeries.getId());
- }
+ mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId());
}
mRecordedProgramPresenter.unbindAllViewHolders();
}
@@ -265,7 +260,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
for (SeriesRecording series : seriesRecordings) {
if (series.getId() == mSeries.getId()) {
- mSeries = null;
getActivity().finish();
return;
}
@@ -372,4 +366,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
return program.getId();
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
index c2c0f596..af6ecc19 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
@@ -14,9 +14,8 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
-import android.app.Activity;
import android.content.Context;
import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
@@ -34,16 +33,16 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.List;
/**
* Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}.
*/
-public class SeriesRecordingPresenter extends DvrItemPresenter {
+class SeriesRecordingPresenter extends DvrItemPresenter {
private final ChannelDataManager mChannelDataManager;
private final DvrDataManager mDvrDataManager;
private final DvrManager mDvrManager;
diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
index d28f026c..5abd52a1 100644
--- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java
@@ -29,7 +29,7 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
/**
* A base fragment to show the list of schedule recordings.
diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
index f6e6ac26..a0410bb3 100644
--- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.list;
import android.app.Activity;
import android.app.ProgressDialog;
@@ -24,15 +24,13 @@ import android.support.annotation.IntDef;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Program;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.SeriesRecordingScheduler;
-import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
-import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+import com.android.tv.dvr.ui.BigArguments;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -72,33 +70,47 @@ public class DvrSchedulesActivity extends Activity {
getFragmentManager().beginTransaction().add(
R.id.fragment_container, schedulesFragment).commit();
} else if (scheduleType == TYPE_SERIES_SCHEDULE) {
- final ProgressDialog dialog = ProgressDialog.show(this, null, getString(
- R.string.dvr_series_schedules_progress_message_reading_programs));
- SeriesRecording seriesRecording = getIntent().getExtras()
- .getParcelable(DvrSeriesSchedulesFragment
- .SERIES_SCHEDULES_KEY_SERIES_RECORDING);
- // To get programs faster, hold the update of the series schedules.
- SeriesRecordingScheduler.getInstance(this).pauseUpdate();
- new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) {
- @Override
- protected void onPostExecute(List<Program> programs) {
- SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate();
- dialog.dismiss();
- Bundle args = getIntent().getExtras();
- args.putParcelableArrayList(DvrSeriesSchedulesFragment
- .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs));
- DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment();
- schedulesFragment.setArguments(args);
- getFragmentManager().beginTransaction().add(
- R.id.fragment_container, schedulesFragment).commit();
- }
- }.setLoadCurrentProgram(true)
- .setLoadDisallowedProgram(true)
- .setLoadScheduledEpisode(true)
- .setIgnoreChannelOption(true)
- .execute();
+ if (BigArguments.getArgument(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) {
+ // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need
+ // to reset the BigArguments.
+ showDvrSeriesSchedulesFragment(getIntent().getExtras());
+ } else {
+ final ProgressDialog dialog = ProgressDialog.show(this, null, getString(
+ R.string.dvr_series_progress_message_reading_programs));
+ SeriesRecording seriesRecording = getIntent().getExtras()
+ .getParcelable(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ // To get programs faster, hold the update of the series schedules.
+ SeriesRecordingScheduler.getInstance(this).pauseUpdate();
+ new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this)
+ .resumeUpdate();
+ dialog.dismiss();
+ Bundle args = getIntent().getExtras();
+ BigArguments.reset();
+ BigArguments.setArgument(
+ DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS,
+ programs == null ? Collections.EMPTY_LIST : programs);
+ showDvrSeriesSchedulesFragment(args);
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true)
+ .execute();
+ }
} else {
finish();
}
}
+
+ private void showDvrSeriesSchedulesFragment(Bundle args) {
+ DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment();
+ schedulesFragment.setArguments(args);
+ getFragmentManager().beginTransaction().add(
+ R.id.fragment_container, schedulesFragment).commit();
+ }
}
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
index 722c9b6e..3cbb500a 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
@@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list;
import android.os.Bundle;
import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
/**
diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
index 42a1e72b..57e7a88f 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -17,6 +17,7 @@
package com.android.tv.dvr.ui.list;
import android.annotation.TargetApi;
+import android.content.Context;
import android.database.ContentObserver;
import android.media.tv.TvContract.Programs;
import android.net.Uri;
@@ -35,11 +36,13 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
+import com.android.tv.dvr.ui.BigArguments;
+import java.util.Collections;
import java.util.List;
/**
@@ -47,20 +50,22 @@ import java.util.List;
*/
@TargetApi(Build.VERSION_CODES.N)
public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
- private static final String TAG = "DvrSeriesSchedulesFragment";
/**
* The key for series recording whose scheduled recording list will be displayed.
+ * Type: {@link SeriesRecording}
*/
public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING =
"series_schedules_key_series_recording";
/**
- * The key for programs belong to the series recording whose scheduled recording
- * list will be displayed.
+ * The key for programs which belong to the series recording whose scheduled recording list
+ * will be displayed.
+ * Type: List<{@link Program}>
*/
public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS =
"series_schedules_key_series_programs";
private ChannelDataManager mChannelDataManager;
+ private DvrDataManager mDvrDataManager;
private SeriesRecording mSeriesRecording;
private List<Program> mPrograms;
private EpisodicProgramLoadTask mProgramLoadTask;
@@ -87,20 +92,22 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
&& getRowsAdapter() instanceof SeriesScheduleRowAdapter) {
((SeriesScheduleRowAdapter) getRowsAdapter())
.onSeriesRecordingUpdated(r);
+ mSeriesRecording = r;
+ updateEmptyMessage();
return;
}
}
}
};
- private final ContentObserver mContentObserver =
- new ContentObserver(new Handler(Looper.getMainLooper())) {
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- super.onChange(selfChange, uri);
- executeProgramLoadingTask();
- }
- };
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final ContentObserver mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ executeProgramLoadingTask();
+ }
+ };
private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() {
@Override
@@ -120,17 +127,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
}
@Override
- public void onCreate(Bundle savedInstanceState) {
+ public void onAttach(Context context) {
+ super.onAttach(context);
Bundle args = getArguments();
if (args != null) {
mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING);
- mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS);
+ mPrograms = (List<Program>) BigArguments.getArgument(
+ SERIES_SCHEDULES_KEY_SERIES_PROGRAMS);
+ BigArguments.reset();
}
+ if (args == null || mPrograms == null) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
- singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener);
mChannelDataManager = singletons.getChannelDataManager();
mChannelDataManager.addListener(mChannelListener);
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener);
getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
mContentObserver);
}
@@ -144,8 +162,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
private void onProgramsUpdated() {
((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms);
+ updateEmptyMessage();
+ }
+
+ private void updateEmptyMessage() {
if (mPrograms == null || mPrograms.isEmpty()) {
- showEmptyMessage(R.string.dvr_series_schedules_empty_state);
+ if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) {
+ showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state);
+ } else {
+ showEmptyMessage(R.string.dvr_series_schedules_empty_state);
+ }
} else {
hideEmptyMessage();
}
@@ -158,15 +184,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
mProgramLoadTask = null;
}
getContext().getContentResolver().unregisterContentObserver(mContentObserver);
+ mHandler.removeCallbacksAndMessages(null);
mChannelDataManager.removeListener(mChannelListener);
- TvApplication.getSingletons(getContext()).getDvrDataManager()
- .removeSeriesRecordingListener(mSeriesRecordingListener);
+ mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener);
super.onDestroy();
}
@Override
public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() {
- return new SeriesRecordingHeaderRowPresenter(getContext());
+ return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext());
}
@Override
@@ -195,7 +221,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
@Override
protected void onPostExecute(List<Program> programs) {
- mPrograms = programs;
+ mPrograms = programs == null ? Collections.EMPTY_LIST : programs;
onProgramsUpdated();
}
};
@@ -205,4 +231,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
.setIgnoreChannelOption(true)
.execute();
}
-} \ No newline at end of file
+}
diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
index 23aebf59..9b0ad105 100644
--- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
+++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
@@ -19,13 +19,13 @@ package com.android.tv.dvr.ui.list;
import android.content.Context;
import com.android.tv.data.Program;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.ScheduledRecording.Builder;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.Builder;
/**
* A class for the episodic program.
*/
-public class EpisodicProgramRow extends ScheduleRow {
+class EpisodicProgramRow extends ScheduleRow {
private final String mInputId;
private final Program mProgram;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
index 3fc92e8a..66e96f94 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
@@ -20,12 +20,12 @@ import android.content.Context;
import android.support.annotation.Nullable;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
/**
* A class for schedule recording row.
*/
-public class ScheduleRow {
+class ScheduleRow {
private final SchedulesHeaderRow mHeaderRow;
@Nullable private ScheduledRecording mSchedule;
private boolean mStopRecordingRequested;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index 9cc82653..97d60473 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -30,8 +30,8 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit;
/**
* An adapter for {@link ScheduleRow}.
*/
-public class ScheduleRowAdapter extends ArrayObjectAdapter {
+class ScheduleRowAdapter extends ArrayObjectAdapter {
private static final String TAG = "ScheduleRowAdapter";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 1257e725..dc4e3c41 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -42,25 +42,24 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* A RowPresenter for {@link ScheduleRow}.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class ScheduleRowPresenter extends RowPresenter {
+class ScheduleRowPresenter extends RowPresenter {
private static final String TAG = "ScheduleRowPresenter";
@Retention(RetentionPolicy.SOURCE)
@@ -345,7 +344,9 @@ public class ScheduleRowPresenter extends RowPresenter {
viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- onInfoClicked(row);
+ if (isInfoClickable(row)) {
+ onInfoClicked(row);
+ }
}
});
@@ -366,8 +367,7 @@ public class ScheduleRowPresenter extends RowPresenter {
viewHolder.mTimeView.setText(onGetRecordingTimeText(row));
String programInfoText = onGetProgramInfoText(row);
if (TextUtils.isEmpty(programInfoText)) {
- int durationMins =
- Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1);
+ int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration()));
programInfoText = mContext.getResources().getQuantityString(
R.plurals.dvr_schedules_recording_duration, durationMins, durationMins);
}
@@ -403,6 +403,7 @@ public class ScheduleRowPresenter extends RowPresenter {
} else {
viewHolder.whiteBackInfo();
}
+ viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
updateActionContainer(viewHolder, viewHolder.isSelected());
}
@@ -454,11 +455,13 @@ public class ScheduleRowPresenter extends RowPresenter {
/**
* Called when user click Info in {@link ScheduleRow}.
*/
- protected void onInfoClicked(ScheduleRow scheduleRow) {
- ScheduledRecording schedule = scheduleRow.getSchedule();
- if (schedule != null) {
- DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true);
- }
+ protected void onInfoClicked(ScheduleRow row) {
+ DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true);
+ }
+
+ private boolean isInfoClickable(ScheduleRow row) {
+ return row.getSchedule() != null
+ && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress());
}
/**
@@ -545,7 +548,7 @@ public class ScheduleRowPresenter extends RowPresenter {
// This row has been deleted.
return;
}
- if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
+ if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
row.setStopRecordingRequested(true);
mDvrManager.stopRecording(row.getSchedule());
CharSequence deletedInfo = onGetProgramInfoText(row);
@@ -670,10 +673,9 @@ public class ScheduleRowPresenter extends RowPresenter {
hideActionView(viewHolder.mFirstActionContainer, View.GONE);
}
};
- if (mLastFocusedViewId == R.id.action_first_container
- || mLastFocusedViewId == R.id.action_second_container) {
- mLastFocusedViewId = R.id.info_container;
- }
+ mLastFocusedViewId = R.id.info_container;
+ SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG,
+ "No focusable view in this row: " + viewHolder);
break;
}
View view = viewHolder.view.findViewById(mLastFocusedViewId);
@@ -683,8 +685,10 @@ public class ScheduleRowPresenter extends RowPresenter {
// requestFocus() explicitly.
if (view.hasFocus()) {
viewHolder.mPendingAnimationRunnable.run();
- } else {
+ } else if (view.isFocusable()){
view.requestFocus();
+ } else {
+ viewHolder.view.requestFocus();
}
}
} else {
@@ -737,10 +741,10 @@ public class ScheduleRowPresenter extends RowPresenter {
@ScheduleRowAction
protected int[] getAvailableActions(ScheduleRow row) {
if (row.getSchedule() != null) {
- if (row.isOnAir()) {
- if (row.isRecordingInProgress()) {
- return new int[] {ACTION_STOP_RECORDING};
- } else if (row.isRecordingNotStarted()) {
+ if (row.isRecordingInProgress()) {
+ return new int[]{ACTION_STOP_RECORDING};
+ } else if (row.isOnAir()) {
+ if (row.isRecordingNotStarted()) {
if (canResolveConflict()) {
// The "START" action can change the conflict states.
return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING};
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
index 0fb0924d..715ecb8c 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
@@ -16,12 +16,15 @@
package com.android.tv.dvr.ui.list;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.data.SeriesRecording;
+
+import java.util.List;
/**
* A base class for the rows for schedules' header.
*/
-public abstract class SchedulesHeaderRow {
+abstract class SchedulesHeaderRow {
private String mTitle;
private String mDescription;
private int mItemCount;
@@ -98,11 +101,20 @@ public abstract class SchedulesHeaderRow {
*/
public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow {
private SeriesRecording mSeriesRecording;
+ private List<Program> mPrograms;
public SeriesRecordingHeaderRow(String title, String description, int itemCount,
- SeriesRecording series) {
+ SeriesRecording series, List<Program> programs) {
super(title, description, itemCount);
mSeriesRecording = series;
+ mPrograms = programs;
+ }
+
+ /**
+ * Returns the list of programs which belong to the series.
+ */
+ public List<Program> getPrograms() {
+ return mPrograms;
}
/**
@@ -119,4 +131,4 @@ public abstract class SchedulesHeaderRow {
mSeriesRecording = seriesRecording;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index 69c33a96..fe2033ba 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -30,15 +30,14 @@ import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.ui.DvrSchedulesActivity;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
/**
* A base class for RowPresenter for {@link SchedulesHeaderRow}
*/
-public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
+abstract class SchedulesHeaderRowPresenter extends RowPresenter {
private Context mContext;
public SchedulesHeaderRowPresenter(Context context) {
@@ -79,7 +78,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
/**
- * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}.
*/
public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter {
public DateHeaderRowPresenter(Context context) {
@@ -93,7 +92,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
/**
* A ViewHolder for
- * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ * {@link SchedulesHeaderRow.DateHeaderRow}.
*/
public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder {
public DateHeaderRowViewHolder(Context context, ViewGroup parent) {
@@ -152,9 +151,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
- // TODO: pass channel list for settings.
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- header.getSeriesRecording().getId(), null, false, false, false);
+ header.getSeriesRecording().getId(),
+ header.getPrograms(), false, false, false, null);
}
});
headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() {
@@ -169,9 +168,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
.build();
TvApplication.getSingletons(getContext()).getDvrManager()
.updateSeriesRecording(seriesRecording);
- // TODO: pass channel list for settings.
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- header.getSeriesRecording().getId(), null, false, false, false);
+ header.getSeriesRecording().getId(),
+ header.getPrograms(), false, false, false, null);
} else {
DvrUiHelper.showCancelAllSeriesRecordingDialog(
(DvrSchedulesActivity) view.getContext(),
@@ -182,11 +181,8 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
private void setTextDrawable(TextView textView, Drawable drawableStart) {
- if (mLtr) {
- textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null,
+ null);
}
/**
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
index 3b493774..6b6de8b8 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
import com.android.tv.util.Utils;
@@ -46,7 +46,7 @@ import java.util.Map;
* An adapter for series schedule row.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
+class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
private static final String TAG = "SeriesRowAdapter";
private static final boolean DEBUG = false;
@@ -96,7 +96,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
Collections.sort(sortedPrograms);
List<EpisodicProgramRow> rows = new ArrayList<>();
mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(),
- null, sortedPrograms.size(), mSeriesRecording);
+ null, sortedPrograms.size(), mSeriesRecording, programs);
for (Program program : sortedPrograms) {
ScheduledRecording schedule =
mDataManager.getScheduledRecordingForProgramId(program.getId());
@@ -145,7 +145,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
if (index != -1) {
EpisodicProgramRow row = (EpisodicProgramRow) get(index);
if (!row.isStartRecordingRequested()) {
- row.setSchedule(schedule);
+ setScheduleToRow(row, schedule);
notifyArrayItemRangeChanged(index, 1);
}
}
@@ -195,12 +195,10 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
if (!isStartOrStopRequested()) {
executePendingUpdate();
}
- row.setSchedule(schedule);
+ setScheduleToRow(row, schedule);
}
- } else if (willBeKept(schedule)) {
- row.setSchedule(schedule);
} else {
- row.setSchedule(null);
+ setScheduleToRow(row, schedule);
}
notifyArrayItemRangeChanged(index, 1);
}
@@ -213,6 +211,14 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
}
}
+ private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) {
+ if (schedule != null && willBeKept(schedule)) {
+ row.setSchedule(schedule);
+ } else {
+ row.setSchedule(null);
+ }
+ }
+
private int findRowIndexByProgramId(long programId) {
for (int i = 0; i < size(); i++) {
Object item = get(i);
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
index 5d88579a..c8503e0d 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
@@ -22,13 +22,13 @@ import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.Utils;
/**
* A RowPresenter for series schedule row.
*/
-public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
+class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
private static final String TAG = "SeriesRowPresenter";
private boolean mLtr;
@@ -74,13 +74,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext()
.getResources().getDimensionPixelOffset(
R.dimen.dvr_schedules_warning_icon_padding));
- if (mLtr) {
- viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
- R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
- } else {
- viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
- 0, 0, R.drawable.ic_warning_gray600_36dp, 0);
- }
+ viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
} else {
viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
@@ -88,9 +83,7 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
@Override
protected void onInfoClicked(ScheduleRow row) {
- if (row.getSchedule() != null) {
- DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule());
- }
+ DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule());
}
@Override
diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index 5deda44a..2437d1f5 100644
--- a/src/com/android/tv/dvr/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
import android.content.Intent;
@@ -24,7 +24,7 @@ import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
+import com.android.tv.dvr.data.RecordedProgram;
/**
* Activity to play a {@link RecordedProgram}.
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java
index 8c4c856c..4bd121b1 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.playback;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@@ -26,22 +25,25 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.DvrPlaybackActivity;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.browse.RecordedProgramPresenter;
+import com.android.tv.dvr.ui.browse.RecordingCardView;
import com.android.tv.util.Utils;
/**
* This class is used to generate Views and bind Objects for related recordings in DVR playback.
*/
-public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
+class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
private static final String TAG = "DvrPlaybackCardPresenter";
private static final boolean DEBUG = false;
private final int mRelatedRecordingCardWidth;
private final int mRelatedRecordingCardHeight;
+ private final DvrPlaybackOverlayFragment mFragment;
- DvrPlaybackCardPresenter(Context context) {
+ DvrPlaybackCardPresenter(Context context, DvrPlaybackOverlayFragment fragment) {
super(context);
+ mFragment = fragment;
mRelatedRecordingCardWidth =
context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width);
mRelatedRecordingCardHeight =
@@ -50,9 +52,8 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
- Resources res = parent.getResources();
RecordingCardView view = new RecordingCardView(
- getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight);
+ getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true);
return new ViewHolder(view);
}
@@ -61,6 +62,10 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
return new OnClickListener() {
@Override
public void onClick(View v) {
+ // Disable fading of overlay fragment to prevent the layout blinking while updating
+ // new playback states and info. The fading enabled status will be reset during
+ // playback state changing, in DvrPlaybackControlHelper.onStateChanged().
+ mFragment.setFadingEnabled(false);
long programId = ((RecordedProgram) v.getTag()).getId();
if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId);
Intent intent = new Intent(getContext(), DvrPlaybackActivity.class);
@@ -69,14 +74,4 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
}
};
}
-
- @Override
- protected String getDescription(RecordedProgram program) {
- String description = program.getDescription();
- if (TextUtils.isEmpty(description)) {
- description =
- getContext().getResources().getString(R.string.dvr_msg_no_program_description);
- }
- return description;
- }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 0bc4ecb1..4658a328 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -14,19 +14,26 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
+import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaController.TransportControls;
import android.media.session.PlaybackState;
+import android.media.tv.TvTrackInfo;
+import android.os.Bundle;
import android.support.v17.leanback.app.PlaybackControlGlue;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction;
+import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
import android.support.v17.leanback.widget.RowPresenter;
import android.text.TextUtils;
@@ -37,19 +44,18 @@ import android.view.View;
import com.android.tv.R;
import com.android.tv.util.TimeShiftUtils;
+import java.util.ArrayList;
+
/**
* A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and
* send command to the media controller. It also helps to update playback states displayed in the
* fragment according to information the media session provides.
*/
-public class DvrPlaybackControlHelper extends PlaybackControlGlue {
+class DvrPlaybackControlHelper extends PlaybackControlGlue {
private static final String TAG = "DvrPlaybackControlHelper";
private static final boolean DEBUG = false;
- /**
- * Indicates the ID of the media under playback is unknown.
- */
- public static int UNKNOWN_MEDIA_ID = -1;
+ private static final int AUDIO_ACTION_ID = 1001;
private int mPlaybackState = PlaybackState.STATE_NONE;
private int mPlaybackSpeedLevel;
@@ -60,6 +66,9 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
private final TransportControls mTransportControls;
private final int mExtraPaddingTopForNoDescription;
+ private final ArrayObjectAdapter mSecondaryActionsAdapter;
+ private final MultiAction mClosedCaptioningAction;
+ private final MultiAction mMultiAudioAction;
public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
@@ -68,11 +77,15 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
mTransportControls = mMediaController.getTransportControls();
mExtraPaddingTopForNoDescription = activity.getResources()
.getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
+ mSecondaryActionsAdapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector());
+ mClosedCaptioningAction = new ClosedCaptioningAction(activity);
+ mMultiAudioAction = new MultiAudioAction(activity);
}
@Override
public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+ controlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter);
setControlsRow(controlsRow);
AbstractDetailsDescriptionPresenter detailsPresenter =
new AbstractDetailsDescriptionPresenter() {
@@ -116,7 +129,21 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
@Override
public void onActionClicked(Action action) {
if (mReadyToControl) {
- DvrPlaybackControlHelper.super.onActionClicked(action);
+ int trackType;
+ if (action.getId() == mClosedCaptioningAction.getId()) {
+ trackType = TvTrackInfo.TYPE_SUBTITLE;
+ } else if (action.getId() == AUDIO_ACTION_ID) {
+ trackType = TvTrackInfo.TYPE_AUDIO;
+ } else {
+ DvrPlaybackControlHelper.super.onActionClicked(action);
+ return;
+ }
+ ArrayList<TvTrackInfo> trackInfos =
+ ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType);
+ if (!trackInfos.isEmpty()) {
+ showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment)
+ getFragment()).getSelectedTrackId(trackType));
+ }
}
}
});
@@ -158,10 +185,10 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
/**
* Returns the ID of the media under playback.
*/
- public long getMediaId() {
+ public String getMediaId() {
MediaMetadata mediaMetadata = mMediaController.getMetadata();
- return mediaMetadata == null ? UNKNOWN_MEDIA_ID
- : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ return mediaMetadata == null ? null
+ : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
}
@Override
@@ -217,6 +244,37 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
mMediaController.unregisterCallback(mMediaControllerCallback);
}
+ /**
+ * Update the secondary controls row.
+ * @param hasClosedCaption {@code true} to show the closed caption selection button,
+ * {@code false} to hide it.
+ * @param hasMultiAudio {@code true} to show the audio track selection button,
+ * {@code false} to hide it.
+ */
+ public void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) {
+ if (hasClosedCaption) {
+ if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) {
+ mSecondaryActionsAdapter.add(0, mClosedCaptioningAction);
+ }
+ } else {
+ mSecondaryActionsAdapter.remove(mClosedCaptioningAction);
+ }
+ if (hasMultiAudio) {
+ if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) {
+ mSecondaryActionsAdapter.add(mMultiAudioAction);
+ }
+ } else {
+ mSecondaryActionsAdapter.remove(mMultiAudioAction);
+ }
+ }
+
+ /**
+ * Returns if the secondary controls row has any buttons and thus should be shown.
+ */
+ public boolean hasSecondaryRow() {
+ return mSecondaryActionsAdapter.size() != 0;
+ }
+
@Override
protected void startPlayback(int speedId) {
if (getCurrentSpeedId() == speedId) {
@@ -251,6 +309,14 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
// Do nothing.
}
+ /**
+ * Notifies closed caption being enabled/disabled to update related UI.
+ */
+ void onSubtitleTrackStateChanged(boolean enabled) {
+ mClosedCaptioningAction.setIndex(enabled ?
+ ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF);
+ }
+
private void onStateChanged(int state, long positionMs, int speedLevel) {
if (DEBUG) Log.d(TAG, "onStateChanged");
getControlsRow().setCurrentTime((int) positionMs);
@@ -297,6 +363,19 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
onStateChanged();
}
+ private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) {
+ Bundle args = new Bundle();
+ args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos);
+ args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId);
+ DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment();
+ sideFragment.setArguments(args);
+ getFragment().getFragmentManager().beginTransaction()
+ .hide(getFragment())
+ .replace(R.id.dvr_playback_side_fragment, sideFragment)
+ .addToBackStack(null)
+ .commit();
+ }
+
private class MediaControllerCallback extends MediaController.Callback {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
@@ -310,4 +389,11 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated();
}
}
+
+ private static class MultiAudioAction extends MultiAction {
+ MultiAudioAction(Context context) {
+ super(AUDIO_ACTION_ID);
+ setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)});
+ }
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index 9759a856..843d2dbe 100644
--- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
import android.content.Intent;
@@ -31,14 +31,16 @@ import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TimeShiftUtils;
import com.android.tv.util.Utils;
-public class DvrPlaybackMediaSessionHelper {
+class DvrPlaybackMediaSessionHelper {
private static final String TAG = "DvrPlaybackMediaSessionHelper";
private static final boolean DEBUG = false;
@@ -102,6 +104,7 @@ public class DvrPlaybackMediaSessionHelper {
}
if (mMediaSession != null) {
mMediaSession.release();
+ mMediaSession = null;
}
}
@@ -179,83 +182,88 @@ public class DvrPlaybackMediaSessionHelper {
cardTitleText = (channel != null) ? channel.getDisplayName()
: mActivity.getString(R.string.no_program_information);
}
- updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(),
- mProgramDurationMs, null, 0);
+ final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText,
+ program.getDescription(), mProgramDurationMs);
String posterArtUri = program.getPosterArtUri();
if (posterArtUri == null) {
posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString();
}
- updatePosterArt(program, cardTitleText, program.getDescription(),
- mProgramDurationMs, null, posterArtUri);
+ updatePosterArt(program, currentMetadata, null, posterArtUri);
mMediaSession.setActive(true);
}
- private void updatePosterArt(RecordedProgram program, String cardTitleText,
- String cardSubtitleText, long duration,
+ private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata,
@Nullable Bitmap posterArt, @Nullable String posterArtUri) {
if (posterArt != null) {
- updateMediaMetadata(program.getId(), cardTitleText,
- cardSubtitleText, duration, posterArt, 0);
+ updateMetadataImageInfo(program, currentMetadata, posterArt, 0);
} else if (posterArtUri != null) {
ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth,
- mNowPlayingCardHeight, new ProgramPosterArtCallback(
- mActivity, program, cardTitleText, cardSubtitleText, duration));
+ mNowPlayingCardHeight,
+ new ProgramPosterArtCallback(mActivity, program, currentMetadata));
} else {
- updateMediaMetadata(program.getId(), cardTitleText,
- cardSubtitleText, duration, null, R.drawable.default_now_card);
+ updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card);
}
}
private class ProgramPosterArtCallback extends
ImageLoader.ImageLoaderCallback<Activity> {
- private RecordedProgram mRecordedProgram;
- private String mCardTitleText;
- private String mCardSubtitleText;
- private long mDuration;
+ private final RecordedProgram mRecordedProgram;
+ private final MediaMetadata mCurrentMetadata;
public ProgramPosterArtCallback(Activity activity, RecordedProgram program,
- String cardTitleText, String cardSubtitleText, long duration) {
+ MediaMetadata metadata) {
super(activity);
mRecordedProgram = program;
- mCardTitleText = cardTitleText;
- mCardSubtitleText = cardSubtitleText;
- mDuration = duration;
+ mCurrentMetadata = metadata;
}
@Override
public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) {
if (isCurrentProgram(mRecordedProgram)) {
- updatePosterArt(mRecordedProgram, mCardTitleText,
- mCardSubtitleText, mDuration, posterArt, null);
+ updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null);
}
}
}
- private void updateMediaMetadata(final long programId, final String title,
- final String subtitle, final long duration,
- final Bitmap posterArt, final int imageResId) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... arg0) {
- MediaMetadata.Builder builder = new MediaMetadata.Builder();
- builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId)
- .putString(MediaMetadata.METADATA_KEY_TITLE, title)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
- if (subtitle != null) {
- builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
- }
- Bitmap programPosterArt = posterArt;
- if (programPosterArt == null && imageResId != 0) {
- programPosterArt =
- BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
- }
- if (programPosterArt != null) {
- builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
- }
+ private MediaMetadata updateMetadataTextInfo(final long programId, final String title,
+ final String subtitle, final long duration) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId))
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
+ if (subtitle != null) {
+ builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+ }
+ MediaMetadata metadata = builder.build();
+ mMediaSession.setMetadata(metadata);
+ return metadata;
+ }
+
+ private void updateMetadataImageInfo(final RecordedProgram program,
+ final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) {
+ if (mMediaSession != null && (posterArt != null || imageResId != 0)) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata);
+ if (posterArt != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
mMediaSession.setMetadata(builder.build());
- return null;
+ } else {
+ new AsyncTask<Void, Void, Bitmap>() {
+ @Override
+ protected Bitmap doInBackground(Void... arg0) {
+ return BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap programPosterArt) {
+ if (mMediaSession != null && programPosterArt != null
+ && isCurrentProgram(program)) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+ }.execute();
}
- }.execute();
+ }
}
// An event was triggered by MediaController.TransportControls and must be handled here.
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
index 51ec93b8..ff907182 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -14,13 +14,14 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.playback;
import android.content.Context;
import android.content.Intent;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
import android.media.tv.TvContentRating;
+import android.media.tv.TvTrackInfo;
import android.os.Bundle;
import android.media.session.PlaybackState;
import android.media.tv.TvInputManager;
@@ -30,7 +31,6 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
import android.support.v17.leanback.widget.SinglePresenterSelector;
@@ -38,20 +38,25 @@ import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
-import android.text.TextUtils;
import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.BaseProgram;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrPlayer;
-import com.android.tv.dvr.DvrPlaybackMediaSessionHelper;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
+import com.android.tv.dvr.ui.browse.DvrListRowPresenter;
import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.util.TvSettings;
+import com.android.tv.util.TvTrackInfoUtils;
import com.android.tv.util.Utils;
+import java.util.List;
+import java.util.ArrayList;
+
public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
// TODO: Handles audio focus. Deals with block and ratings.
private static final String TAG = "DvrPlaybackOverlayFragment";
@@ -62,6 +67,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
// mProgram is only used to store program from intent. Don't use it elsewhere.
private RecordedProgram mProgram;
+ private DvrPlayer mDvrPlayer;
private DvrPlaybackMediaSessionHelper mMediaSessionHelper;
private DvrPlaybackControlHelper mPlaybackControlHelper;
private ArrayObjectAdapter mRowsAdapter;
@@ -72,19 +78,30 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
private TvView mTvView;
private View mBlockScreenView;
private ListRow mRelatedRecordingsRow;
- private int mExtraPaddingNoRelatedRow;
+ private int mPaddingWithoutRelatedRow;
+ private int mPaddingWithoutSecondaryRow;
private int mWindowWidth;
private int mWindowHeight;
private float mAppliedAspectRatio;
private float mWindowAspectRatio;
private boolean mPinChecked;
+ private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener =
+ new DvrPlayer.OnTrackSelectedListener() {
+ @Override
+ public void onTrackSelected(String selectedTrackId) {
+ mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null);
+ mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
+ }
+ };
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
- mExtraPaddingNoRelatedRow = getActivity().getResources()
- .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top);
+ mPaddingWithoutRelatedRow = getActivity().getResources()
+ .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row);
+ mPaddingWithoutSecondaryRow = getActivity().getResources()
+ .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row);
mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
mContentRatingsManager = TvApplication.getSingletons(getContext())
.getTvInputManagerHelper().getContentRatingsManager();
@@ -110,13 +127,31 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
super.onActivityCreated(savedInstanceState);
mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view);
mBlockScreenView = getActivity().findViewById(R.id.block_screen);
+ mDvrPlayer = new DvrPlayer(mTvView);
mMediaSessionHelper = new DvrPlaybackMediaSessionHelper(
- getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this);
+ getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this);
mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this);
setUpRows();
- preparePlayback(getActivity().getIntent());
- DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer();
- dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() {
+ mDvrPlayer.setOnTracksAvailabilityChangedListener(
+ new DvrPlayer.OnTracksAvailabilityChangedListener() {
+ @Override
+ public void onTracksAvailabilityChanged(boolean hasClosedCaption,
+ boolean hasMultiAudio) {
+ mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio);
+ if (hasClosedCaption) {
+ mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE,
+ mOnSubtitleTrackSelectedListener);
+ selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE);
+ } else {
+ mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null);
+ }
+ if (hasMultiAudio) {
+ selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO);
+ }
+ onMediaControllerUpdated();
+ }
+ });
+ mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() {
@Override
public void onAspectRatioChanged(float videoAspectRatio) {
updateAspectRatio(videoAspectRatio);
@@ -124,7 +159,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
});
mPinChecked = getActivity().getIntent()
.getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false);
- dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() {
+ mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() {
@Override
public void onContentBlocked(TvContentRating rating) {
if (mPinChecked) {
@@ -149,6 +184,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
.show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
}
});
+ preparePlayback(getActivity().getIntent());
}
@Override
@@ -200,6 +236,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
updateAspectRatio(mAppliedAspectRatio);
}
+ /**
+ * Returns next recorded episode in the same series as now playing program.
+ */
public RecordedProgram getNextEpisode(RecordedProgram program) {
int position = mRelatedRecordingsRowAdapter.findInsertPosition(program);
if (position == mRelatedRecordingsRowAdapter.size()) {
@@ -209,16 +248,92 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
}
}
+ /**
+ * Returns the tracks of the give type of the current playback.
+
+ * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE}
+ * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}.
+ */
+ public ArrayList<TvTrackInfo> getTracks(int trackType) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ return mDvrPlayer.getAudioTracks();
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ return mDvrPlayer.getSubtitleTracks();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the ID of the selected track of the given type.
+ */
+ public String getSelectedTrackId(int trackType) {
+ return mDvrPlayer.getSelectedTrackId(trackType);
+ }
+
+ /**
+ * Returns the language setting of the given track type.
+
+ * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE}
+ * or {@link TvTrackInfo#TYPE_AUDIO}.
+ * @return {@code null} if no language has been set for the given track type.
+ */
+ TvTrackInfo getTrackSetting(int trackType) {
+ return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType);
+ }
+
+ /**
+ * Selects the given audio or subtitle track for DVR playback.
+ * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE}
+ * or {@link TvTrackInfo#TYPE_AUDIO}.
+ * @param selectedTrack {@code null} to disable the audio or subtitle track according to
+ * trackType.
+ */
+ void selectTrack(int trackType, TvTrackInfo selectedTrack) {
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.selectTrack(trackType, selectedTrack);
+ }
+ }
+
+ /**
+ * Notifies the content of controls row or related recordings row is changed and the UI should
+ * be updated according to the change.
+ */
void onMediaControllerUpdated() {
- mRowsAdapter.notifyArrayItemRangeChanged(0, 1);
+ updateVerticalPosition();
+ mRowsAdapter.notifyArrayItemRangeChanged(0, 2);
+ }
+
+ private void selectBestMatchedTrack(int trackType) {
+ TvTrackInfo selectedTrack = getTrackSetting(trackType);
+ if (selectedTrack != null) {
+ TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType),
+ selectedTrack.getId(), selectedTrack.getLanguage(),
+ trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0);
+ if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils
+ .isEqualLanguage(bestMatchedTrack.getLanguage(),
+ selectedTrack.getLanguage()))) {
+ selectTrack(trackType, bestMatchedTrack);
+ return;
+ }
+ }
+ if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ // Disables closed captioning if there's no matched language.
+ selectTrack(TvTrackInfo.TYPE_SUBTITLE, null);
+ }
}
private void updateAspectRatio(float videoAspectRatio) {
+ if (videoAspectRatio <= 0) {
+ // We don't have video's width or height information, use window's aspect ratio.
+ videoAspectRatio = mWindowAspectRatio;
+ }
if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) {
// No need to change
return;
}
- if (videoAspectRatio < mWindowAspectRatio) {
+ if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) {
+ ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0);
+ } else if (videoAspectRatio < mWindowAspectRatio) {
int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2;
((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0);
} else {
@@ -230,6 +345,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
private void preparePlayback(Intent intent) {
mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent));
+ mPlaybackControlHelper.updateSecondaryRow(false, false);
getActivity().getMediaController().getTransportControls().prepare();
updateRelatedRecordingsRow();
}
@@ -239,24 +355,35 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
mRelatedRecordingsRowAdapter.clear();
long programId = mProgram.getId();
String seriesId = mProgram.getSeriesId();
- if (!TextUtils.isEmpty(seriesId)) {
+ SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ if (seriesRecording != null) {
if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId);
- for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) {
- if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) {
+ List<RecordedProgram> relatedPrograms =
+ mDvrDataManager.getRecordedPrograms(seriesRecording.getId());
+ for (RecordedProgram program : relatedPrograms) {
+ if (programId != program.getId()) {
mRelatedRecordingsRowAdapter.add(program);
}
}
}
- View view = getView();
if (mRelatedRecordingsRowAdapter.size() == 0) {
mRowsAdapter.remove(mRelatedRecordingsRow);
- view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow,
- view.getPaddingRight(), view.getPaddingBottom());
} else if (wasEmpty){
mRowsAdapter.add(mRelatedRecordingsRow);
- view.setPadding(view.getPaddingLeft(), 0,
- view.getPaddingRight(), view.getPaddingBottom());
}
+ onMediaControllerUpdated();
+ }
+
+ private void updateVerticalPosition() {
+ int verticalPadding = 0;
+ verticalPadding +=
+ mRelatedRecordingsRowAdapter.size() == 0 ? mPaddingWithoutRelatedRow : 0;
+ verticalPadding +=
+ mPlaybackControlHelper.hasSecondaryRow() ? 0 : mPaddingWithoutSecondaryRow;
+ if (DEBUG) Log.d(TAG, "New controls padding: " + verticalPadding);
+ View view = getView();
+ view.setPadding(view.getPaddingLeft(), verticalPadding,
+ view.getPaddingRight(), view.getPaddingBottom());
}
private void setUpRows() {
@@ -265,7 +392,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
ClassPresenterSelector selector = new ClassPresenterSelector();
selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter);
- selector.addClassPresenter(ListRow.class, new ListRowPresenter());
+ selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
mRowsAdapter = new ArrayObjectAdapter(selector);
mRowsAdapter.add(mPlaybackControlHelper.getControlsRow());
@@ -274,7 +401,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
}
private ListRow getRelatedRecordingsRow() {
- mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity());
+ mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this);
mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter);
HeaderItem header = new HeaderItem(0,
getActivity().getString(R.string.dvr_playback_related_recordings));
@@ -297,7 +424,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment {
}
@Override
- long getId(BaseProgram item) {
+ public long getId(BaseProgram item) {
return item.getId();
}
}
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java
new file mode 100644
index 00000000..e49870f1
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.playback;
+
+import android.media.tv.TvTrackInfo;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.text.TextUtils;
+import android.transition.Transition;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.util.TvSettings;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Fragment for DVR playback closed-caption/multi-audio settings.
+ */
+public class DvrPlaybackSideFragment extends GuidedStepFragment {
+ /**
+ * The tag for passing track infos to side fragments.
+ */
+ public static final String TRACK_INFOS = "dvr_key_track_infos";
+ /**
+ * The tag for passing selected track's ID to side fragments.
+ */
+ public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id";
+
+ private static final int ACTION_ID_NO_SUBTITLE = -1;
+ private static final int CHECK_SET_ID = 1;
+
+ private List<TvTrackInfo> mTrackInfos;
+ private String mSelectedTrackId;
+ private TvTrackInfo mSelectedTrack;
+ private int mTrackType;
+ private DvrPlaybackOverlayFragment mOverlayFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS);
+ mTrackType = mTrackInfos.get(0).getType();
+ mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID);
+ mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager()
+ .findFragmentById(R.id.dvr_playback_controls_fragment));
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState);
+ backgroundView.setBackgroundColor(getResources()
+ .getColor(R.color.lb_playback_controls_background_light));
+ return backgroundView;
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_NO_SUBTITLE)
+ .title(getString(R.string.closed_caption_option_item_off))
+ .checkSetId(CHECK_SET_ID)
+ .checked(mSelectedTrackId == null)
+ .build());
+ }
+ for (int i = 0; i < mTrackInfos.size(); i++) {
+ TvTrackInfo info = mTrackInfos.get(i);
+ boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId);
+ GuidedAction action = new GuidedAction.Builder(getActivity())
+ .id(i)
+ .title(getTrackLabel(info, i))
+ .checkSetId(CHECK_SET_ID)
+ .checked(checked)
+ .build();
+ actions.add(action);
+ if (checked) {
+ mSelectedTrack = info;
+ }
+ }
+ }
+
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ int actionId = (int) action.getId();
+ mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId));
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ int actionId = (int) action.getId();
+ mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId);
+ TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack);
+ getFragmentManager().popBackStack();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Workaround: when overlay fragment is faded out, any focus will lost due to overlay
+ // fragment's implementation. So we disable overlay fragment's fading here to prevent
+ // losing focus while users are interacting with the side fragment.
+ mOverlayFragment.setFadingEnabled(false);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ // We disable fading of overlay fragment to prevent side fragment from losing focus,
+ // therefore we should resume it here.
+ mOverlayFragment.setFadingEnabled(true);
+ mOverlayFragment.selectTrack(mTrackType, mSelectedTrack);
+ }
+
+ private String getTrackLabel(TvTrackInfo track, int trackIndex) {
+ if (track.getLanguage() != null) {
+ return new Locale(track.getLanguage()).getDisplayName();
+ }
+ return track.getType() == TvTrackInfo.TYPE_SUBTITLE ?
+ getString(R.string.closed_caption_unknown_language, trackIndex + 1)
+ : getString(R.string.multi_audio_unknown_language);
+ }
+
+ @Override
+ protected void onProvideFragmentTransitions() {
+ super.onProvideFragmentTransitions();
+ // Excludes the background scrim from transition to prevent the blinking caused by
+ // hiding the overlay fragment and sliding in the side fragment at the same time.
+ Transition t = getEnterTransition();
+ if (t != null) {
+ t.excludeTarget(R.id.guidedstep_background, true);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index 5656655c..780bfb2f 100644
--- a/src/com/android/tv/dvr/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.media.PlaybackParams;
import android.media.tv.TvContentRating;
@@ -22,12 +22,16 @@ import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.media.session.PlaybackState;
+import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.dvr.data.RecordedProgram;
+
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-public class DvrPlayer {
+class DvrPlayer {
private static final String TAG = "DvrPlayer";
private static final boolean DEBUG = false;
@@ -47,12 +51,19 @@ public class DvrPlayer {
private long mInitialSeekPositionMs;
private final TvView mTvView;
private DvrPlayerCallback mCallback;
- private AspectRatioChangedListener mAspectRatioChangedListener;
- private ContentBlockedListener mContentBlockedListener;
+ private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
+ private OnContentBlockedListener mOnContentBlockedListener;
+ private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener;
+ private OnTrackSelectedListener mOnAudioTrackSelectedListener;
+ private OnTrackSelectedListener mOnSubtitleTrackSelectedListener;
+ private String mSelectedAudioTrackId;
+ private String mSelectedSubtitleTrackId;
private float mAspectRatio = Float.NaN;
private int mPlaybackState = PlaybackState.STATE_NONE;
private long mTimeShiftCurrentPositionMs;
private boolean mPauseOnPrepared;
+ private boolean mHasClosedCaption;
+ private boolean mHasMultiAudio;
private final PlaybackParams mPlaybackParams = new PlaybackParams();
private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback();
private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
@@ -75,22 +86,40 @@ public class DvrPlayer {
public void onPlaybackEnded() { }
}
- public interface AspectRatioChangedListener {
+ public interface OnAspectRatioChangedListener {
/**
* Called when the Video's aspect ratio is changed.
+ *
+ * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios.
+ * Listeners should handle it carefully.
*/
void onAspectRatioChanged(float videoAspectRatio);
}
- public interface ContentBlockedListener {
+ public interface OnContentBlockedListener {
/**
* Called when the Video's aspect ratio is changed.
*/
void onContentBlocked(TvContentRating rating);
}
+ public interface OnTracksAvailabilityChangedListener {
+ /**
+ * Called when the Video's subtitle or audio tracks are changed.
+ */
+ void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
+ }
+
+ public interface OnTrackSelectedListener {
+ /**
+ * Called when certain subtitle or audio track is selected.
+ */
+ void onTrackSelected(String selectedTrackId);
+ }
+
public DvrPlayer(TvView tvView) {
mTvView = tvView;
+ mTvView.setCaptionEnabled(true);
mPlaybackParams.setSpeed(1.0f);
setTvViewCallbacks();
setCallback(null);
@@ -236,6 +265,8 @@ public class DvrPlayer {
mTimeShiftCurrentPositionMs = 0;
mPlaybackParams.setSpeed(1.0f);
mProgram = null;
+ mSelectedAudioTrackId = null;
+ mSelectedSubtitleTrackId = null;
}
/**
@@ -250,17 +281,51 @@ public class DvrPlayer {
}
/**
- * Sets listener to aspect ratio changing.
+ * Sets the listener to aspect ratio changing.
+ */
+ public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) {
+ mOnAspectRatioChangedListener = listener;
+ }
+
+ /**
+ * Sets the listener to content blocking.
+ */
+ public void setOnContentBlockedListener(OnContentBlockedListener listener) {
+ mOnContentBlockedListener = listener;
+ }
+
+ /**
+ * Sets the listener to tracks changing.
+ */
+ public void setOnTracksAvailabilityChangedListener(
+ OnTracksAvailabilityChangedListener listener) {
+ mOnTracksAvailabilityChangedListener = listener;
+ }
+
+ /**
+ * Sets the listener to tracks of the given type being selected.
+ *
+ * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO}
+ * or {@link TvTrackInfo#TYPE_SUBTITLE}.
*/
- public void setAspectRatioChangedListener(AspectRatioChangedListener listener) {
- mAspectRatioChangedListener = listener;
+ public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ mOnAudioTrackSelectedListener = listener;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ mOnSubtitleTrackSelectedListener = listener;
+ }
}
/**
- * Sets listener to content blocking.
+ * Gets the listener to tracks of the given type being selected.
*/
- public void setContentBlockedListener(ContentBlockedListener listener) {
- mContentBlockedListener = listener;
+ public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ return mOnAudioTrackSelectedListener;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ return mOnSubtitleTrackSelectedListener;
+ }
+ return null;
}
/**
@@ -306,6 +371,32 @@ public class DvrPlayer {
}
/**
+ * Returns the subtitle tracks of the current playback.
+ */
+ public ArrayList<TvTrackInfo> getSubtitleTracks() {
+ return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE));
+ }
+
+ /**
+ * Returns the audio tracks of the current playback.
+ */
+ public ArrayList<TvTrackInfo> getAudioTracks() {
+ return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO));
+ }
+
+ /**
+ * Returns the ID of the selected track of the given type.
+ */
+ public String getSelectedTrackId(int trackType) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ return mSelectedAudioTrackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ return mSelectedSubtitleTrackId;
+ }
+ return null;
+ }
+
+ /**
* Returns if playback of the recorded program is started.
*/
public boolean isPlaybackPrepared() {
@@ -313,6 +404,41 @@ public class DvrPlayer {
&& mPlaybackState != PlaybackState.STATE_CONNECTING;
}
+ /**
+ * Selects the given track.
+ *
+ * @return ID of the selected track.
+ */
+ String selectTrack(int trackType, TvTrackInfo selectedTrack) {
+ String oldSelectedTrackId = getSelectedTrackId(trackType);
+ String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId();
+ if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) {
+ if (selectedTrack == null) {
+ mTvView.selectTrack(trackType, null);
+ return null;
+ } else {
+ List<TvTrackInfo> tracks = mTvView.getTracks(trackType);
+ if (tracks != null && tracks.contains(selectedTrack)) {
+ mTvView.selectTrack(trackType, newSelectedTrackId);
+ return newSelectedTrackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) {
+ // Track not found, disabled closed caption.
+ mTvView.selectTrack(trackType, null);
+ return null;
+ }
+ }
+ }
+ return oldSelectedTrackId;
+ }
+
+ private void setSelectedTrackId(int trackType, String trackId) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ mSelectedAudioTrackId = trackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ mSelectedSubtitleTrackId = trackId;
+ }
+ }
+
private void setPlaybackSpeed(int speed) {
mPlaybackParams.setSpeed(speed);
mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
@@ -369,28 +495,60 @@ public class DvrPlayer {
if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
&& mPlaybackState == PlaybackState.STATE_CONNECTING) {
mTimeShiftPlayAvailable = true;
+ if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ // onTimeShiftStatusChanged is sometimes called after
+ // onTimeShiftStartPositionChanged is called. In this case,
+ // resumeToWatchedPositionIfNeeded needs to be called here.
+ resumeToWatchedPositionIfNeeded();
+ }
}
}
@Override
- public void onTrackSelected(String inputId, int type, String trackId) {
- if (trackId == null || type != TvTrackInfo.TYPE_VIDEO
- || mAspectRatioChangedListener == null) {
- return;
+ public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
+ boolean hasClosedCaption =
+ !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty();
+ boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1;
+ if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio)
+ && mOnTracksAvailabilityChangedListener != null) {
+ mOnTracksAvailabilityChangedListener
+ .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio);
}
- List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
- if (trackInfos != null) {
- for (TvTrackInfo trackInfo : trackInfos) {
- if (trackInfo.getId().equals(trackId)) {
- float videoAspectRatio = trackInfo.getVideoPixelAspectRatio()
- * trackInfo.getVideoWidth() / trackInfo.getVideoHeight();
- if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
- if (!Float.isNaN(videoAspectRatio)
- && mAspectRatio != videoAspectRatio) {
- mAspectRatioChangedListener
- .onAspectRatioChanged(videoAspectRatio);
- mAspectRatio = videoAspectRatio;
- return;
+ mHasClosedCaption = hasClosedCaption;
+ mHasMultiAudio = hasMultiAudio;
+ }
+
+ @Override
+ public void onTrackSelected(String inputId, int type, String trackId) {
+ if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) {
+ setSelectedTrackId(type, trackId);
+ OnTrackSelectedListener listener = getOnTrackSelectedListener(type);
+ if (listener != null) {
+ listener.onTrackSelected(trackId);
+ }
+ } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null
+ && mOnAspectRatioChangedListener != null) {
+ List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
+ if (trackInfos != null) {
+ for (TvTrackInfo trackInfo : trackInfos) {
+ if (trackInfo.getId().equals(trackId)) {
+ float videoAspectRatio;
+ int videoWidth = trackInfo.getVideoWidth();
+ int videoHeight = trackInfo.getVideoHeight();
+ if (videoWidth > 0 && videoHeight > 0) {
+ videoAspectRatio = trackInfo.getVideoPixelAspectRatio()
+ * trackInfo.getVideoWidth() / trackInfo.getVideoHeight();
+ } else {
+ // Aspect ratio is unknown. Pass the message to listeners.
+ videoAspectRatio = 0;
+ }
+ if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
+ if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) {
+ mOnAspectRatioChangedListener
+ .onAspectRatioChanged(videoAspectRatio);
+ mAspectRatio = videoAspectRatio;
+ return;
+ }
}
}
}
@@ -399,8 +557,8 @@ public class DvrPlayer {
@Override
public void onContentBlocked(String inputId, TvContentRating rating) {
- if (mContentBlockedListener != null) {
- mContentBlockedListener.onContentBlocked(rating);
+ if (mOnContentBlockedListener != null) {
+ mOnContentBlockedListener.onContentBlocked(rating);
}
}
});
diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java
index 8f60c2b5..c0cbd643 100644
--- a/src/com/android/tv/experiments/ExperimentFlag.java
+++ b/src/com/android/tv/experiments/ExperimentFlag.java
@@ -16,12 +16,19 @@
package com.android.tv.experiments;
+import android.support.annotation.VisibleForTesting;
/**
* Experiments return values based on user, device and other criteria.
*/
public final class ExperimentFlag<T> {
- private final T mDefaultValue;
+
+ private static boolean sAllowOverrides = false;
+
+ @VisibleForTesting
+ public static void initForTest() {
+ sAllowOverrides = true;
+ }
/** Returns a boolean experiment */
public static ExperimentFlag<Boolean> createFlag(
@@ -30,6 +37,11 @@ public final class ExperimentFlag<T> {
defaultValue);
}
+ private final T mDefaultValue;
+
+ private T mOverrideValue = null;
+ private boolean mOverridden = false;
+
private ExperimentFlag(
T defaultValue) {
mDefaultValue = defaultValue;
@@ -37,6 +49,22 @@ public final class ExperimentFlag<T> {
/** Returns value for this experiment */
public T get() {
- return mDefaultValue;
+ return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue;
}
+
+ @VisibleForTesting
+ public void override(T t) {
+ if (sAllowOverrides) {
+ mOverridden = true;
+ mOverrideValue = t;
+ }
+ }
+
+ @VisibleForTesting
+ public void resetOverride() {
+ mOverridden = false;
+ }
+
+
+
}
diff --git a/src/com/android/tv/experiments/Experiments.java b/src/com/android/tv/experiments/Experiments.java
index f16c8d1e..e17fc300 100644
--- a/src/com/android/tv/experiments/Experiments.java
+++ b/src/com/android/tv/experiments/Experiments.java
@@ -23,11 +23,16 @@ import com.android.tv.common.BuildConfig;
/**
* Set of experiments visible in AOSP.
*
- * <p>
- * This file is maintained by hand.
+ * <p>This file is maintained by hand.
*/
public final class Experiments {
public static final ExperimentFlag<Boolean> CLOUD_EPG = createFlag(
+ true);
+
+ /**
+ * Use network tuner if it is available and there is no other tuner types.
+ */
+ public static final ExperimentFlag<Boolean> NETWORK_TUNER = createFlag(
false);
/**
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 120b3dba..bec4e462 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -48,7 +48,7 @@ import com.android.tv.ChannelTuner;
import com.android.tv.Features;
import com.android.tv.MainActivity;
import com.android.tv.R;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.data.ChannelDataManager;
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 4c7a4404..d5fb418f 100644
--- a/src/com/android/tv/guide/ProgramItemView.java
+++ b/src/com/android/tv/guide/ProgramItemView.java
@@ -44,8 +44,8 @@ import com.android.tv.analytics.Tracker;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.guide.ProgramManager.TableEntry;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
@@ -106,18 +106,19 @@ public class ProgramItemView extends TextView {
}, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0
: view.getResources()
.getInteger(R.integer.program_guide_ripple_anim_duration));
- } else if (CommonFeatures.DVR.isEnabled(view.getContext())) {
+ } else if (entry.program != null && CommonFeatures.DVR.isEnabled(view.getContext())) {
DvrManager dvrManager = singletons.getDvrManager();
if (entry.entryStartUtcMillis > System.currentTimeMillis()
&& dvrManager.isProgramRecordable(entry.program)) {
if (entry.scheduledRecording == null) {
- 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());
- ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT);
- }
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity,
+ channel.getInputId(), new Runnable() {
+ @Override
+ public void run() {
+ DvrUiHelper.requestRecordingFutureProgram(tvActivity,
+ entry.program, false);
+ }
+ });
} else {
dvrManager.removeScheduledRecording(entry.scheduledRecording);
String msg = view.getResources().getString(
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index e3d919df..e543fd05 100644
--- a/src/com/android/tv/guide/ProgramManager.java
+++ b/src/com/android/tv/guide/ProgramManager.java
@@ -29,7 +29,7 @@ import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
@@ -439,11 +439,24 @@ public class ProgramManager {
mChannels = mChannelDataManager.getBrowsableChannelList();
mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
mFilteredChannels = mChannels;
+ updateTableEntriesWithoutNotification(clearPreviousTableEntries);
+ // Channel update notification should be called after updating table entries, so that
+ // the listener can get the entries.
notifyChannelsUpdated();
- updateTableEntries(clearPreviousTableEntries);
+ notifyTableEntriesUpdated();
+ buildGenreFilters();
}
private void updateTableEntries(boolean clear) {
+ updateTableEntriesWithoutNotification(clear);
+ notifyTableEntriesUpdated();
+ buildGenreFilters();
+ }
+
+ /**
+ * Updates the table entries without notifying the change.
+ */
+ private void updateTableEntriesWithoutNotification(boolean clear) {
if (clear) {
mChannelIdEntriesMap.clear();
}
@@ -491,9 +504,6 @@ public class ProgramManager {
}
}
}
-
- notifyTableEntriesUpdated();
- buildGenreFilters();
}
private void notifyGenresUpdated() {
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index e4a67972..b9a0593d 100644
--- a/src/com/android/tv/guide/ProgramTableAdapter.java
+++ b/src/com/android/tv/guide/ProgramTableAdapter.java
@@ -45,19 +45,21 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
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.dvr.data.ScheduledRecording;
import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
@@ -241,16 +243,6 @@ 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 {
@@ -312,11 +304,40 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
private final ImageView mInputLogoView;
private boolean mIsInputLogoVisible;
+ private AccessibilityStateChangeListener mAccessibilityStateChangeListener =
+ new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enable) {
+ enable &= !TvCommonUtils.isRunningInTest();
+ mDetailView.setFocusable(enable);
+ mChannelHeaderView.setFocusable(enable);
+ }
+ };
public ProgramRowHolder(View itemView) {
super(itemView);
mContainer = (ViewGroup) itemView;
+ mContainer.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mContainer
+ .getViewTreeObserver()
+ .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ mAccessibilityManager.addAccessibilityStateChangeListener(
+ mAccessibilityStateChangeListener);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mContainer
+ .getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ mAccessibilityManager.removeAccessibilityStateChangeListener(
+ mAccessibilityStateChangeListener);
+ }
+ });
mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row);
mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail);
@@ -339,16 +360,11 @@ 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);
- }
- });
+ // TODO: Find a better way to handle talk back.
+ boolean accessibilityEnabled = mAccessibilityManager.isEnabled()
+ && !TvCommonUtils.isRunningInTest();
+ mDetailView.setFocusable(accessibilityEnabled);
+ mChannelHeaderView.setFocusable(accessibilityEnabled);
}
public void onBind(int position) {
@@ -508,16 +524,6 @@ 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.
diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java
index 868fed46..82907a2d 100644
--- a/src/com/android/tv/guide/TimeListAdapter.java
+++ b/src/com/android/tv/guide/TimeListAdapter.java
@@ -16,6 +16,7 @@
package com.android.tv.guide;
+import android.content.Context;
import android.content.res.Resources;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateFormat;
@@ -25,8 +26,10 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.R;
+import com.android.tv.util.Utils;
import java.util.Date;
+import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
@@ -35,16 +38,28 @@ import java.util.concurrent.TimeUnit;
*/
public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> {
private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30);
+
+ // Ex. 3:00 AM
+ private static final String TIME_PATTERN_SAME_DAY = "h:mm a";
+ // Ex. Oct 21, 3:00 AM
+ private static final String TIME_PATTERN_DIFFERENT_DAY = "MMM d, h:mm a";
+
private static int sRowHeaderOverlapping;
// Nearest half hour at or before the start time.
private long mStartUtcMs;
+ private final String mTimePatternSameDay;
+ private final String mTimePatternDifferentDay;
public TimeListAdapter(Resources res) {
if (sRowHeaderOverlapping == 0) {
sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset(
R.dimen.program_guide_table_header_row_overlap));
}
+ Locale locale = res.getConfiguration().locale;
+ mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY);
+ mTimePatternDifferentDay =
+ DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_DIFFERENT_DAY);
}
public void update(long startTimeMs) {
@@ -68,10 +83,14 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi
long endTime = startTime + TIME_UNIT_MS;
View itemView = holder.itemView;
-
- TextView textView = (TextView) itemView.findViewById(R.id.time);
- String time = DateFormat.getTimeFormat(itemView.getContext()).format(new Date(startTime));
- textView.setText(time);
+ Date timeDate = new Date(startTime);
+ String timeString;
+ if (Utils.isInGivenDay(System.currentTimeMillis(), startTime)) {
+ timeString = DateFormat.format(mTimePatternSameDay, timeDate).toString();
+ } else {
+ timeString = DateFormat.format(mTimePatternDifferentDay, timeDate).toString();
+ }
+ ((TextView) itemView.findViewById(R.id.time)).setText(timeString);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
lp.width = GuideUtils.convertMillisToPixel(startTime, endTime);
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index 54892cac..2fd70bfb 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -19,8 +19,8 @@ package com.android.tv.menu;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
-import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import com.android.tv.R;
@@ -28,7 +28,7 @@ import com.android.tv.R;
/**
* A view to render an item of TV options.
*/
-public class ActionCardView extends FrameLayout implements ItemListRowView.CardView<MenuAction> {
+public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView<MenuAction> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -66,7 +66,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV
}
mIconView.setImageDrawable(action.getDrawable(getContext()));
mLabelView.setText(action.getActionName(getContext()));
- mStateView.setText(action.getActionDescription(getContext()));
+ mStateView.setText(action.getActionDescription());
if (action.isEnabled()) {
setEnabled(true);
setFocusable(true);
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index bfb5e3f1..d23d9a00 100644
--- a/src/com/android/tv/menu/AppLinkCardView.java
+++ b/src/com/android/tv/menu/AppLinkCardView.java
@@ -24,6 +24,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
@@ -55,7 +56,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
private final int mIconColorFilter;
private ImageView mImageView;
- private View mGradientView;
private TextView mAppInfoView;
private View mMetaViewHolder;
private Channel mChannel;
@@ -102,35 +102,115 @@ public class AppLinkCardView extends BaseCardView<Channel> {
int linkType = mChannel.getAppLinkType(getContext());
mIntent = mChannel.getAppLinkIntent(getContext());
+ CharSequence appLabel = null;
+ mImageView.setForeground(null);
switch (linkType) {
case Channel.APP_LINK_TYPE_CHANNEL:
setText(mChannel.getAppLinkText());
mAppInfoView.setVisibility(VISIBLE);
- mGradientView.setVisibility(VISIBLE);
mAppInfoView.setCompoundDrawablePadding(mIconPadding);
- mAppInfoView.setCompoundDrawables(null, null, null, null);
- mAppInfoView.setText(mPackageManager.getApplicationLabel(appInfo));
+ mAppInfoView.setCompoundDrawablesRelative(null, null, null, null);
+ appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(channel.getInputId());
+ if (appLabel != null) {
+ mAppInfoView.setText(appLabel);
+ } else {
+ new AsyncTask<Void, Void, CharSequence>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+
+ @Override
+ protected CharSequence doInBackground(Void... params) {
+ if (appInfo != null) {
+ return mPackageManager.getApplicationLabel(appInfo);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence appLabel) {
+ mTvInputManagerHelper.setTvInputApplicationLabel(
+ mLoadTvInputId, appLabel);
+ if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) {
+ return;
+ }
+ mAppInfoView.setText(appLabel);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
if (!TextUtils.isEmpty(mChannel.getAppLinkIconUri())) {
mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON,
- mIconWidth, mIconHeight, createChannelLogoCallback(this, mChannel,
- Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON));
+ mIconWidth, mIconHeight,
+ createChannelLogoCallback(
+ this, mChannel, Channel.LOAD_IMAGE_TYPE_APP_LINK_ICON));
} else if (appInfo.icon != 0) {
- Drawable appIcon = mPackageManager.getApplicationIcon(appInfo);
- BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
- appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
- mAppInfoView.setCompoundDrawables(appIcon, null, null, null);
+ Drawable appIcon =
+ mTvInputManagerHelper.getTvInputApplicationIcon(mChannel.getInputId());
+ if (appIcon != null) {
+ BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
+ appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
+ mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null);
+ } else {
+ new AsyncTask<Void, Void, Drawable>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ if (appInfo != null) {
+ return mPackageManager.getApplicationIcon(appInfo);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Drawable appIcon) {
+ mTvInputManagerHelper.setTvInputApplicationIcon(
+ mLoadTvInputId, appIcon);
+ if (mLoadTvInputId != mChannel.getInputId()
+ || !isAttachedToWindow()) {
+ return;
+ }
+ BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
+ appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
+ mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
}
break;
case Channel.APP_LINK_TYPE_APP:
- setText(getContext().getString(
- R.string.channels_item_app_link_app_launcher,
- mPackageManager.getApplicationLabel(appInfo)));
+ appLabel = mTvInputManagerHelper.getTvInputApplicationLabel(mChannel.getInputId());
+ if (appLabel != null) {
+ setText(getContext()
+ .getString(R.string.channels_item_app_link_app_launcher, appLabel));
+ } else {
+ new AsyncTask<Void, Void, CharSequence>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+
+ @Override
+ protected CharSequence doInBackground(Void... params) {
+ if (appInfo != null) {
+ return mPackageManager.getApplicationLabel(appInfo);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence appLabel) {
+ mTvInputManagerHelper.setTvInputApplicationLabel(
+ mLoadTvInputId, appLabel);
+ if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) {
+ return;
+ }
+ setText(getContext()
+ .getString(
+ R.string.channels_item_app_link_app_launcher,
+ appLabel));
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
mAppInfoView.setVisibility(GONE);
- mGradientView.setVisibility(GONE);
break;
default:
mAppInfoView.setVisibility(GONE);
- mGradientView.setVisibility(GONE);
Log.d(TAG, "Should not be here.");
}
@@ -148,8 +228,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
} else {
setCardImageWithBanner(appInfo);
}
- // 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);
}
@@ -182,13 +260,14 @@ public class AppLinkCardView extends BaseCardView<Channel> {
}
}
BitmapUtils.setColorFilterToDrawable(mIconColorFilter, drawable);
- mAppInfoView.setCompoundDrawables(drawable, null, null, null);
+ mAppInfoView.setCompoundDrawablesRelative(drawable, null, null, null);
} else if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART) {
if (bitmap == null) {
setCardImageWithBanner(
mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId()));
} else {
mImageView.setImageBitmap(bitmap);
+ mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
if (mChannel.getAppLinkColor() == 0) {
extractAndSetMetaViewBackgroundColor(bitmap);
}
@@ -200,7 +279,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
- mGradientView = findViewById(R.id.image_gradient);
mAppInfoView = (TextView) findViewById(R.id.app_info);
mMetaViewHolder = findViewById(R.id.app_link_text_holder);
}
@@ -209,37 +287,85 @@ public class AppLinkCardView extends BaseCardView<Channel> {
// 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner,
// 5) Application icon, and 6) default image.
private void setCardImageWithBanner(ApplicationInfo appInfo) {
- Drawable banner = null;
- if (mIntent != null) {
- try {
- banner = mPackageManager.getActivityBanner(mIntent);
- if (banner == null) {
- banner = mPackageManager.getActivityIcon(mIntent);
+ new AsyncTask<Void, Void, Drawable>() {
+ private String mLoadTvInputId = mChannel.getInputId();
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ Drawable banner = null;
+ if (mIntent != null) {
+ try {
+ banner = mPackageManager.getActivityBanner(mIntent);
+ if (banner == null) {
+ banner = mPackageManager.getActivityIcon(mIntent);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // do nothing.
+ }
}
- } catch (PackageManager.NameNotFoundException e) {
- // do nothing.
+ return banner;
}
- }
- if (banner == null && appInfo != null) {
- if (appInfo.banner != 0) {
- banner = mPackageManager.getApplicationBanner(appInfo);
- }
- if (banner == null && appInfo.icon != 0) {
- banner = mPackageManager.getApplicationIcon(appInfo);
+ @Override
+ protected void onPostExecute(Drawable banner) {
+ if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) {
+ return;
+ }
+ if (banner != null) {
+ setCardImageWithBannerInternal(banner);
+ } else {
+ setCardImageWithApplicationInfoBanner(appInfo);
+ }
}
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void setCardImageWithApplicationInfoBanner(ApplicationInfo appInfo) {
+ Drawable appBanner =
+ mTvInputManagerHelper.getTvInputApplicationBanner(mChannel.getInputId());
+ if (appBanner != null) {
+ setCardImageWithBannerInternal(appBanner);
+ } else {
+ new AsyncTask<Void, Void, Drawable>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ Drawable banner = null;
+ if (appInfo != null) {
+ if (appInfo.banner != 0) {
+ banner = mPackageManager.getApplicationBanner(appInfo);
+ }
+ if (banner == null && appInfo.icon != 0) {
+ banner = mPackageManager.getApplicationIcon(appInfo);
+ }
+ }
+ return banner;
+ }
+
+ @Override
+ protected void onPostExecute(Drawable banner) {
+ mTvInputManagerHelper.setTvInputApplicationBanner(
+ mLoadTvInputId, banner);
+ if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) {
+ return;
+ }
+ setCardImageWithBannerInternal(banner);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
+ }
+ private void setCardImageWithBannerInternal(Drawable banner) {
if (banner == null) {
mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
mImageView.setBackgroundResource(R.color.channel_card);
} else {
- Bitmap bitmap =
- Bitmap.createBitmap(mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888);
+ Bitmap bitmap = Bitmap.createBitmap(
+ mCardImageWidth, mCardImageHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
banner.setBounds(0, 0, mCardImageWidth, mCardImageHeight);
banner.draw(canvas);
mImageView.setImageDrawable(banner);
+ mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
if (mChannel.getAppLinkColor() == 0) {
extractAndSetMetaViewBackgroundColor(bitmap);
}
diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java
index c6a34a5d..fa74ce3e 100644
--- a/src/com/android/tv/menu/BaseCardView.java
+++ b/src/com/android/tv/menu/BaseCardView.java
@@ -57,6 +57,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
private TextView mTextViewFocused;
private final int mCardImageWidth;
private final float mCardHeight;
+ private boolean mSelected;
public BaseCardView(Context context) {
this(context, null);
@@ -103,23 +104,9 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
/**
* 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) {
- 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);
}
@@ -128,6 +115,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
@Override
public void onSelected() {
+ mSelected = true;
if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
startFocusAnimation(SCALE_FACTOR_1F);
} else {
@@ -138,6 +126,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
@Override
public void onDeselected() {
+ mSelected = false;
if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
startFocusAnimation(SCALE_FACTOR_0F);
} else {
@@ -156,6 +145,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
if (mTextView != null) {
mTextView.setText(resId);
}
+ onTextViewUpdated();
}
/**
@@ -168,6 +158,22 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
if (mTextView != null) {
mTextView.setText(text);
}
+ onTextViewUpdated();
+ }
+
+ private void onTextViewUpdated() {
+ if (mTextView != null && mTextViewFocused != null) {
+ mTextViewFocused.measure(
+ MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1;
+ if (mExtendViewOnFocus) {
+ setTextViewFocusedAlpha(mSelected ? 1f : 0f);
+ } else {
+ setTextViewFocusedAlpha(1f);
+ }
+ }
+ setFocusAnimatedValue(mSelected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
}
/**
@@ -209,12 +215,18 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
setScaleX(scale);
setScaleY(scale);
setTranslationZ(mFocusTranslationZ * animatedValue);
- if (mExtendViewOnFocus) {
+ if (mTextView != null && mTextViewFocused != null) {
ViewGroup.LayoutParams params = mTextView.getLayoutParams();
- params.height = Math.round(mTextViewHeight
- + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue);
- setTextViewLayoutParams(params);
- setTextViewFocusedAlpha(animatedValue);
+ int height = mExtendViewOnFocus ? Math.round(mTextViewHeight
+ + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue)
+ : (int) mTextViewHeight;
+ if (height != params.height) {
+ params.height = height;
+ setTextViewLayoutParams(params);
+ }
+ if (mExtendViewOnFocus) {
+ setTextViewFocusedAlpha(animatedValue);
+ }
}
}
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index 1c8015a6..4ee56892 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -45,7 +45,6 @@ public class ChannelCardView extends BaseCardView<Channel> {
private final int mCardImageHeight;
private ImageView mImageView;
- private View mGradientView;
private TextView mChannelNumberNameView;
private ProgressBar mProgressBar;
private Channel mChannel;
@@ -71,7 +70,6 @@ public class ChannelCardView extends BaseCardView<Channel> {
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
- mGradientView = findViewById(R.id.image_gradient);
mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name);
mProgressBar = (ProgressBar) findViewById(R.id.progress);
}
@@ -88,7 +86,7 @@ public class ChannelCardView extends BaseCardView<Channel> {
mChannelNumberNameView.setVisibility(VISIBLE);
mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
mImageView.setBackgroundResource(R.color.channel_card);
- mGradientView.setVisibility(View.GONE);
+ mImageView.setForeground(null);
mProgressBar.setVisibility(GONE);
setTextViewEnabled(true);
@@ -101,8 +99,6 @@ public class ChannelCardView extends BaseCardView<Channel> {
}
updateProgramInformation();
- // 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);
}
@@ -123,7 +119,7 @@ public class ChannelCardView extends BaseCardView<Channel> {
private void updatePosterArt(Bitmap posterArt) {
mImageView.setImageBitmap(posterArt);
- mGradientView.setVisibility(View.VISIBLE);
+ mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
}
private void updateProgramInformation() {
diff --git a/src/com/android/tv/menu/ChannelsRow.java b/src/com/android/tv/menu/ChannelsRow.java
index dedf0993..490d73de 100644
--- a/src/com/android/tv/menu/ChannelsRow.java
+++ b/src/com/android/tv/menu/ChannelsRow.java
@@ -26,8 +26,14 @@ import com.android.tv.recommendation.Recommender;
public class ChannelsRow extends ItemListRow {
public static final String ID = ChannelsRow.class.getName();
- private static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5;
- private static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10;
+ /**
+ * Minimum count for recent channels.
+ */
+ public static final int MIN_COUNT_FOR_RECENT_CHANNELS = 5;
+ /**
+ * Maximum count for recent channels.
+ */
+ public static final int MAX_COUNT_FOR_RECENT_CHANNELS = 10;
private Recommender mTvRecommendation;
private ChannelsRowAdapter mChannelsAdapter;
diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java
index c8e1bd05..4ba6a93a 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -31,7 +31,6 @@ import com.android.tv.dvr.DvrDataManager;
import com.android.tv.recommendation.Recommender;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
-import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -130,8 +129,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
@Override
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
- super.onBindViewHolder(viewHolder, position);
-
int viewType = getItemViewType(position);
if (viewType == R.layout.menu_card_guide) {
viewHolder.itemView.setOnClickListener(mGuideOnClickListener);
@@ -147,6 +144,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
viewHolder.itemView.setTag(getItemList().get(position));
viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
}
+ super.onBindViewHolder(viewHolder, position);
}
@Override
diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java
index 4919c595..01257628 100644
--- a/src/com/android/tv/menu/ItemListRowView.java
+++ b/src/com/android/tv/menu/ItemListRowView.java
@@ -28,6 +28,7 @@ import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
+import com.android.tv.util.ViewCache;
import java.util.Collections;
import java.util.List;
@@ -194,9 +195,20 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe
return mItemList.size();
}
+ /**
+ * Returns the position of the item.
+ */
+ protected int getItemPosition(T item) {
+ return mItemList.indexOf(item);
+ }
+
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = mLayoutInflater.inflate(getLayoutResId(viewType), parent, false);
+ int resId = getLayoutResId(viewType);
+ View view = ViewCache.getInstance().getView(resId);
+ if (view == null) {
+ view = mLayoutInflater.inflate(resId, parent, false);
+ }
return new MyViewHolder(view);
}
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 1160a5b5..25e629c1 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -27,23 +27,30 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
import com.android.tv.ChannelTuner;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.TvOptionsManager;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.WeakHandler;
import com.android.tv.menu.MenuRowFactory.PartnerRow;
-import com.android.tv.menu.MenuRowFactory.PipOptionsRow;
import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
import com.android.tv.ui.TunableTvView;
+import com.android.tv.util.DurationTimer;
+import com.android.tv.util.ViewCache;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* A class which controls the menu.
@@ -81,10 +88,21 @@ public class Menu {
sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT
}
+ private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>();
+ static {
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7);
+ }
+
private static final String SCREEN_NAME = "Menu";
private static final int MSG_HIDE_MENU = 1000;
+ private final Context mContext;
private final IMenuView mMenuView;
private final Tracker mTracker;
private final DurationTimer mVisibleTimer = new DurationTimer();
@@ -103,15 +121,16 @@ public class Menu {
@VisibleForTesting
Menu(Context context, IMenuView menuView, MenuRowFactory menuRowFactory,
OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
- this(context, null, menuView, menuRowFactory, onMenuVisibilityChangeListener);
+ this(context, null, null, menuView, menuRowFactory, onMenuVisibilityChangeListener);
}
- public Menu(Context context, TunableTvView tvView, IMenuView menuView,
- MenuRowFactory menuRowFactory,
+ public Menu(Context context, TunableTvView tvView, TvOptionsManager optionsManager,
+ IMenuView menuView, MenuRowFactory menuRowFactory,
OnMenuVisibilityChangeListener onMenuVisibilityChangeListener) {
+ mContext = context;
mMenuView = menuView;
mTracker = TvApplication.getSingletons(context).getTracker();
- mMenuUpdater = new MenuUpdater(context, tvView, this);
+ mMenuUpdater = new MenuUpdater(this, tvView, optionsManager);
Resources res = context.getResources();
mShowDurationMillis = res.getInteger(R.integer.menu_show_duration);
mOnMenuVisibilityChangeListener = onMenuVisibilityChangeListener;
@@ -130,7 +149,6 @@ public class Menu {
addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class));
addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class));
addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class));
- addMenuRow(menuRowFactory.createMenuRow(this, PipOptionsRow.class));
mMenuView.setMenuRows(mMenuRows);
}
@@ -160,6 +178,23 @@ public class Menu {
}
/**
+ * Preloads the item view used for the menu.
+ */
+ public void preloadItemViews() {
+ LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ // Use a fake parent to make the layoutParams set correctly.
+ ViewGroup fakeParent = new LinearLayout(mContext);
+ for (int id : PRELOAD_VIEW_IDS.keySet()) {
+ int count = PRELOAD_VIEW_IDS.get(id);
+ for (int i = 0; i < count; i++) {
+ View view = inflater.inflate(id, fakeParent, false);
+ ViewCache.getInstance().putView(id, view);
+ }
+ }
+ }
+
+ /**
* Shows the main menu.
*
* @param reason A reason why this is called. See {@link MenuShowReason}
diff --git a/src/com/android/tv/menu/MenuAction.java b/src/com/android/tv/menu/MenuAction.java
index 0d59552a..b4356059 100644
--- a/src/com/android/tv/menu/MenuAction.java
+++ b/src/com/android/tv/menu/MenuAction.java
@@ -20,9 +20,9 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
-import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvOptionsManager;
+import com.android.tv.TvOptionsManager.OptionType;
/**
* A class to define possible actions from main menu.
@@ -36,12 +36,9 @@ public class MenuAction {
public static final MenuAction SELECT_DISPLAY_MODE_ACTION =
new MenuAction(R.string.options_item_display_mode, TvOptionsManager.OPTION_DISPLAY_MODE,
R.drawable.ic_tvoption_aspect);
- public static final MenuAction PIP_IN_APP_ACTION =
- new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_IN_APP_PIP,
- R.drawable.ic_tvoption_pip);
public static final MenuAction SYSTEMWIDE_PIP_ACTION =
new MenuAction(R.string.options_item_pip, TvOptionsManager.OPTION_SYSTEMWIDE_PIP,
- R.drawable.ic_pip_option_layout2);
+ R.drawable.ic_tvoption_pip);
public static final MenuAction SELECT_AUDIO_LANGUAGE_ACTION =
new MenuAction(R.string.options_item_multi_audio, TvOptionsManager.OPTION_MULTI_AUDIO,
R.drawable.ic_tvoption_multi_track);
@@ -51,34 +48,36 @@ public class MenuAction {
public static final MenuAction DEV_ACTION =
new MenuAction(R.string.options_item_developer,
TvOptionsManager.OPTION_DEVELOPER, R.drawable.ic_developer_mode_tv_white_48dp);
- // TODO: Change the icon.
public static final MenuAction SETTINGS_ACTION =
new MenuAction(R.string.options_item_settings, TvOptionsManager.OPTION_SETTINGS,
R.drawable.ic_settings);
- // Actions in the PIP option row.
- public static final MenuAction PIP_SELECT_INPUT_ACTION =
- new MenuAction(R.string.pip_options_item_source, TvOptionsManager.OPTION_PIP_INPUT,
- R.drawable.ic_pip_option_input);
- public static final MenuAction PIP_SWAP_ACTION =
- new MenuAction(R.string.pip_options_item_swap, TvOptionsManager.OPTION_PIP_SWAP,
- R.drawable.ic_pip_option_swap);
- public static final MenuAction PIP_SOUND_ACTION =
- new MenuAction(R.string.pip_options_item_sound, TvOptionsManager.OPTION_PIP_SOUND,
- R.drawable.ic_pip_option_swap_audio);
- public static final MenuAction PIP_LAYOUT_ACTION =
- new MenuAction(R.string.pip_options_item_layout, TvOptionsManager.OPTION_PIP_LAYOUT,
- R.drawable.ic_pip_option_layout1);
- public static final MenuAction PIP_SIZE_ACTION =
- new MenuAction(R.string.pip_options_item_size, TvOptionsManager.OPTION_PIP_SIZE,
- R.drawable.ic_pip_option_size);
private final String mActionName;
private final int mActionNameResId;
- private final int mType;
+ @OptionType private final int mType;
+ private String mActionDescription;
private Drawable mDrawable;
private int mDrawableResId;
private boolean mEnabled = true;
+ /**
+ * Sets the action description. Returns {@code trye} if the description is changed.
+ */
+ public static boolean setActionDescription(MenuAction action, String actionDescription) {
+ String oldDescription = action.mActionDescription;
+ action.mActionDescription = actionDescription;
+ return !TextUtils.equals(action.mActionDescription, oldDescription);
+ }
+
+ /**
+ * Enables or disables the action. Returns {@code true} if the value is changed.
+ */
+ public static boolean setEnabled(MenuAction action, boolean enabled) {
+ boolean changed = action.mEnabled != enabled;
+ action.mEnabled = enabled;
+ return changed;
+ }
+
public MenuAction(int actionNameResId, int type, int drawableResId) {
mActionName = null;
mActionNameResId = actionNameResId;
@@ -102,11 +101,11 @@ public class MenuAction {
return context.getString(mActionNameResId);
}
- public String getActionDescription(Context context) {
- return ((MainActivity) context).getTvOptionsManager().getOptionString(mType);
+ public String getActionDescription() {
+ return mActionDescription;
}
- public int getType() {
+ @OptionType public int getType() {
return mType;
}
@@ -120,28 +119,10 @@ public class MenuAction {
return mDrawable;
}
- /**
- * Sets drawable resource id.
- *
- * @return {@code true} if drawable is changed.
- */
- public boolean setDrawableResId(int resId) {
- if (mDrawableResId == resId) {
- return false;
- }
- mDrawable = null;
- mDrawableResId = resId;
- return true;
- }
-
public boolean isEnabled() {
return mEnabled;
}
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
public int getActionNameResId() {
return mActionNameResId;
}
diff --git a/src/com/android/tv/menu/MenuLayoutManager.java b/src/com/android/tv/menu/MenuLayoutManager.java
index 6c767247..a16ac197 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -384,10 +384,15 @@ public class MenuLayoutManager {
mSelectedPosition = position;
if (DEBUG) dumpChildren("startRowAnimation()");
- MenuRowView currentView = mMenuRowViews.get(position);
// Show the children of the next row.
- currentView.getTitleView().setVisibility(View.VISIBLE);
- currentView.getContentsView().setVisibility(View.VISIBLE);
+ final MenuRowView currentView = mMenuRowViews.get(position);
+ TextView currentTitleView = currentView.getTitleView();
+ View currentContentsView = currentView.getContentsView();
+ currentTitleView.setVisibility(View.VISIBLE);
+ currentContentsView.setVisibility(View.VISIBLE);
+ if (currentView instanceof PlayControlsRowView) {
+ ((PlayControlsRowView) currentView).onPreselected();
+ }
// Request focus after the new contents view shows up.
mMenuView.requestFocus();
if (mTempTitleViewForOld == null) {
@@ -407,7 +412,7 @@ public class MenuLayoutManager {
// Old row.
MenuRow oldRow = mMenuRows.get(oldPosition);
- MenuRowView oldView = mMenuRowViews.get(oldPosition);
+ final MenuRowView oldView = mMenuRowViews.get(oldPosition);
View oldContentsView = oldView.getContentsView();
// Old contents view.
animators.add(createAlphaAnimator(oldContentsView, 1.0f, 0.0f, 1.0f, mLinearOutSlowIn)
@@ -468,8 +473,6 @@ public class MenuLayoutManager {
}
// Current row.
Rect currentLayoutRect = new Rect(layouts.get(position));
- TextView currentTitleView = currentView.getTitleView();
- View currentContentsView = currentView.getContentsView();
currentContentsView.setAlpha(0.0f);
if (scrollDown) {
// Current title view.
@@ -572,9 +575,8 @@ public class MenuLayoutManager {
for (ViewPropertyValueHolder holder : propertyValuesAfterAnimation) {
holder.property.set(holder.view, holder.value);
}
- oldTitleView.setVisibility(View.VISIBLE);
- mMenuRowViews.get(oldPosition).onDeselected();
- mMenuRowViews.get(position).onSelected(true);
+ oldView.onDeselected();
+ currentView.onSelected(true);
mTempTitleViewForOld.setVisibility(View.GONE);
mTempTitleViewForCurrent.setVisibility(View.GONE);
layout(mMenuView.getLeft(), mMenuView.getTop(), mMenuView.getRight(),
diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java
index c67a0e04..2d5453fe 100644
--- a/src/com/android/tv/menu/MenuRowFactory.java
+++ b/src/com/android/tv/menu/MenuRowFactory.java
@@ -67,8 +67,6 @@ public class MenuRowFactory {
} else if (TvOptionsRow.class.equals(key)) {
return new TvOptionsRow(mMainActivity, menu, mTvCustomizationManager
.getCustomActions(TvCustomizationManager.ID_OPTIONS_ROW));
- } else if (PipOptionsRow.class.equals(key)) {
- return new PipOptionsRow(mMainActivity, menu);
}
return null;
}
@@ -77,6 +75,9 @@ public class MenuRowFactory {
* A menu row which represents the TV options row.
*/
public static class TvOptionsRow extends ItemListRow {
+ /** The ID of the row. */
+ public static final String ID = TvOptionsRow.class.getName();
+
private TvOptionsRow(Context context, Menu menu, List<CustomAction> customActions) {
super(context, menu, R.string.menu_title_options, R.dimen.action_card_height,
new TvOptionsRowAdapter(context, customActions));
@@ -91,25 +92,6 @@ public class MenuRowFactory {
}
/**
- * A menu row which represents the PIP options row.
- */
- public static class PipOptionsRow extends ItemListRow {
- private final MainActivity mMainActivity;
-
- private PipOptionsRow(Context context, Menu menu) {
- super(context, menu, R.string.menu_title_pip_options, R.dimen.action_card_height,
- new PipOptionsRowAdapter(context));
- mMainActivity = (MainActivity) context;
- }
-
- @Override
- public boolean isVisible() {
- // TODO: Remove the dependency on MainActivity.
- return super.isVisible() && mMainActivity.isPipEnabled();
- }
- }
-
- /**
* A menu row which represents the partner row.
*/
public static class PartnerRow extends ItemListRow {
diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java
index 075b299e..7ad38e74 100644
--- a/src/com/android/tv/menu/MenuUpdater.java
+++ b/src/com/android/tv/menu/MenuUpdater.java
@@ -16,11 +16,14 @@
package com.android.tv.menu;
-import android.content.Context;
import android.support.annotation.Nullable;
import com.android.tv.ChannelTuner;
+import com.android.tv.TvOptionsManager;
+import com.android.tv.TvOptionsManager.OptionChangedListener;
+import com.android.tv.TvOptionsManager.OptionType;
import com.android.tv.data.Channel;
+import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener;
@@ -30,10 +33,10 @@ import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener;
* <p>As the menu is updated when it shows up, this class handles only the dynamic updates.
*/
public class MenuUpdater {
- // Can be null for testing.
- @Nullable
- private final TunableTvView mTvView;
private final Menu mMenu;
+ // Can be null for testing.
+ @Nullable private final TunableTvView mTvView;
+ @Nullable private final TvOptionsManager mOptionsManager;
private ChannelTuner mChannelTuner;
private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
@@ -42,7 +45,7 @@ public class MenuUpdater {
@Override
public void onBrowsableChannelListChanged() {
- mMenu.update();
+ mMenu.update(ChannelsRow.ID);
}
@Override
@@ -53,10 +56,17 @@ public class MenuUpdater {
mMenu.update(ChannelsRow.ID);
}
};
+ private final OptionChangedListener mOptionChangeListener = new OptionChangedListener() {
+ @Override
+ public void onOptionChanged(@OptionType int optionType, String newString) {
+ mMenu.update(TvOptionsRow.ID);
+ }
+ };
- public MenuUpdater(Context context, TunableTvView tvView, Menu menu) {
- mTvView = tvView;
+ public MenuUpdater(Menu menu, TunableTvView tvView, TvOptionsManager optionsManager) {
mMenu = menu;
+ mTvView = tvView;
+ mOptionsManager = optionsManager;
if (mTvView != null) {
mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() {
@Override
@@ -65,11 +75,18 @@ public class MenuUpdater {
}
});
}
+ if (mOptionsManager != null) {
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS,
+ mOptionChangeListener);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE,
+ mOptionChangeListener);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO,
+ mOptionChangeListener);
+ }
}
/**
- * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready
- * or not available any more.
+ * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready.
*/
public void setChannelTuner(ChannelTuner channelTuner) {
if (mChannelTuner != null) {
@@ -79,7 +96,6 @@ public class MenuUpdater {
if (mChannelTuner != null) {
mChannelTuner.addListener(mChannelTunerListener);
}
- mMenu.update();
}
/**
@@ -92,5 +108,10 @@ public class MenuUpdater {
if (mTvView != null) {
mTvView.setOnScreenBlockedListener(null);
}
+ if (mOptionsManager != null) {
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, null);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, null);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, null);
+ }
}
}
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index 93bd0a4d..dd6194a1 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -21,8 +21,6 @@ import android.view.View;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.TvOptionsManager;
-import com.android.tv.TvOptionsManager.OptionChangedListener;
import com.android.tv.analytics.Tracker;
import java.util.List;
@@ -66,12 +64,9 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
public void update() {
if (mActionList == null) {
mActionList = createActions();
- updateActions();
setItemList(mActionList);
} else {
- if (updateActions()) {
- setItemList(mActionList);
- }
+ updateActions();
}
}
@@ -81,7 +76,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
}
protected abstract List<MenuAction> createActions();
- protected abstract boolean updateActions();
+ protected abstract void updateActions();
protected abstract void executeAction(int type);
/**
@@ -93,37 +88,6 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
return mActionList.get(position);
}
- /**
- * Sets the action at the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void setAction(int position, MenuAction action) {
- mActionList.set(position, action);
- }
-
- /**
- * Adds an action to the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void addAction(int position, MenuAction action) {
- mActionList.add(position, action);
- }
-
- /**
- * Removes an action at the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void removeAction(int position) {
- mActionList.remove(position);
- }
-
- protected int getActionSize() {
- return mActionList.size();
- }
-
@Override
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
super.onBindViewHolder(viewHolder, position);
@@ -139,14 +103,4 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
// be preserved.
return mActionList.get(position).getType();
}
-
- protected void setOptionChangedListener(final MenuAction action) {
- TvOptionsManager om = getMainActivity().getTvOptionsManager();
- om.setOptionChangedListener(action.getType(), new OptionChangedListener() {
- @Override
- public void onOptionChanged(String newOption) {
- setItemList(mActionList);
- }
- });
- }
}
diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
index f3e09f80..c8249a4c 100644
--- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
@@ -38,8 +38,7 @@ public class PartnerOptionsRowAdapter extends CustomizableOptionsRowAdapter {
}
@Override
- protected boolean updateActions() {
+ protected void updateActions() {
// TODO: Support adding description for custom actions.
- return false;
}
}
diff --git a/src/com/android/tv/menu/PipOptionsRowAdapter.java b/src/com/android/tv/menu/PipOptionsRowAdapter.java
deleted file mode 100644
index 87203e9d..00000000
--- a/src/com/android/tv/menu/PipOptionsRowAdapter.java
+++ /dev/null
@@ -1,137 +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.text.TextUtils;
-
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-import com.android.tv.TvOptionsManager;
-import com.android.tv.ui.TvViewUiManager;
-import com.android.tv.ui.sidepanel.PipInputSelectorFragment;
-import com.android.tv.util.PipInputManager.PipInput;
-import com.android.tv.util.TvSettings;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/*
- * An adapter of PIP options.
- */
-public class PipOptionsRowAdapter extends OptionsRowAdapter {
- private static final int[] DRAWABLE_ID_FOR_LAYOUT = {
- R.drawable.ic_pip_option_layout1,
- R.drawable.ic_pip_option_layout2,
- R.drawable.ic_pip_option_layout3,
- R.drawable.ic_pip_option_layout4,
- R.drawable.ic_pip_option_layout5 };
-
- private final TvOptionsManager mTvOptionsManager;
- private final TvViewUiManager mTvViewUiManager;
-
- public PipOptionsRowAdapter(Context context) {
- super(context);
- mTvOptionsManager = getMainActivity().getTvOptionsManager();
- mTvViewUiManager = getMainActivity().getTvViewUiManager();
- }
-
- @Override
- protected List<MenuAction> createActions() {
- List<MenuAction> actionList = new ArrayList<>();
- actionList.add(MenuAction.PIP_SELECT_INPUT_ACTION);
- actionList.add(MenuAction.PIP_SWAP_ACTION);
- actionList.add(MenuAction.PIP_SOUND_ACTION);
- actionList.add(MenuAction.PIP_LAYOUT_ACTION);
- actionList.add(MenuAction.PIP_SIZE_ACTION);
- for (MenuAction action : actionList) {
- setOptionChangedListener(action);
- }
- return actionList;
- }
-
- @Override
- public boolean updateActions() {
- boolean changed = false;
- if (updateSelectInputAction()) {
- changed = true;
- }
- if (updateLayoutAction()) {
- changed = true;
- }
- if (updateSizeAction()) {
- changed = true;
- }
- return changed;
- }
-
- private boolean updateSelectInputAction() {
- String oldInputLabel = mTvOptionsManager.getOptionString(TvOptionsManager.OPTION_PIP_INPUT);
-
- MainActivity tvActivity = getMainActivity();
- PipInput newInput = tvActivity.getPipInputManager().getPipInput(tvActivity.getPipChannel());
- String newInputLabel = newInput == null ? null : newInput.getLabel();
-
- if (!TextUtils.equals(oldInputLabel, newInputLabel)) {
- mTvOptionsManager.onPipInputChanged(newInputLabel);
- return true;
- }
- return false;
- }
-
- private boolean updateLayoutAction() {
- return MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(
- DRAWABLE_ID_FOR_LAYOUT[mTvViewUiManager.getPipLayout()]);
- }
-
- private boolean updateSizeAction() {
- boolean oldEnabled = MenuAction.PIP_SIZE_ACTION.isEnabled();
- boolean newEnabled = mTvViewUiManager.getPipLayout() != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE;
- if (oldEnabled != newEnabled) {
- MenuAction.PIP_SIZE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
- }
-
- @Override
- protected void executeAction(int type) {
- switch (type) {
- case TvOptionsManager.OPTION_PIP_INPUT:
- getMainActivity().getOverlayManager().getSideFragmentManager().show(
- new PipInputSelectorFragment());
- break;
- case TvOptionsManager.OPTION_PIP_SWAP:
- getMainActivity().swapPip();
- break;
- case TvOptionsManager.OPTION_PIP_SOUND:
- getMainActivity().togglePipSoundMode();
- break;
- case TvOptionsManager.OPTION_PIP_LAYOUT:
- int oldLayout = mTvViewUiManager.getPipLayout();
- int newLayout = (oldLayout + 1) % (TvSettings.PIP_LAYOUT_LAST + 1);
- mTvViewUiManager.setPipLayout(newLayout, true);
- MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(DRAWABLE_ID_FOR_LAYOUT[newLayout]);
- break;
- case TvOptionsManager.OPTION_PIP_SIZE:
- int oldSize = mTvViewUiManager.getPipSize();
- int newSize = (oldSize + 1) % (TvSettings.PIP_SIZE_LAST + 1);
- mTvViewUiManager.setPipSize(newSize, true);
- break;
- }
- }
-}
diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java
index aff39db3..77715f28 100644
--- a/src/com/android/tv/menu/PlayControlsButton.java
+++ b/src/com/android/tv/menu/PlayControlsButton.java
@@ -39,6 +39,9 @@ public class PlayControlsButton extends FrameLayout {
private final int mIconColor;
private int mIconFocusedColor;
+ private int mImageResourceId;
+ private int mTintColor;
+
public PlayControlsButton(Context context) {
this(context, null);
}
@@ -67,10 +70,21 @@ public class PlayControlsButton extends FrameLayout {
* Sets the resource ID of the image to be displayed in the center of this control.
*/
public void setImageResId(int imageResId) {
- mIcon.setImageResource(imageResId);
- // Since on foucus changing, icons' color should be switched with animation,
+ int newTintColor = hasFocus() ? mIconFocusedColor : mIconColor;
+ if (mImageResourceId != imageResId) {
+ mImageResourceId = imageResId;
+ mIcon.setImageResource(imageResId);
+ updateTint(newTintColor);
+ } else if (newTintColor != mTintColor) {
+ updateTint(newTintColor);
+ }
+ }
+
+ private void updateTint(int tintColor) {
+ mTintColor = tintColor;
+ // Since on focus changing, icons' color should be switched with animation,
// as a result, selectors cannot be used to switch colors in this case.
- mIcon.getDrawable().setTint(hasFocus() ? mIconFocusedColor : mIconColor);
+ mIcon.getDrawable().setTint(tintColor);
}
/**
@@ -117,7 +131,9 @@ public class PlayControlsButton extends FrameLayout {
} else {
mIcon.setVisibility(View.GONE);
mLabel.setVisibility(View.VISIBLE);
- mLabel.setText(label);
+ if (!TextUtils.equals(mLabel.getText(), label)) {
+ mLabel.setText(label);
+ }
}
}
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index a620d4dd..4d766788 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -18,10 +18,10 @@ package com.android.tv.menu;
import android.content.Context;
import android.content.res.Resources;
+import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
@@ -34,17 +34,16 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.menu.Menu.MenuShowReason;
import com.android.tv.ui.TunableTvView;
-import com.android.tv.util.Utils;
public class PlayControlsRowView extends MenuRowView {
private static final int NORMAL_WIDTH_MAX_BUTTON_COUNT = 5;
@@ -53,14 +52,10 @@ public class PlayControlsRowView extends MenuRowView {
private final int mTimeTextLeftMargin;
private final int mTimelineWidth;
// Views
- private View mBackgroundView;
+ private TextView mBackgroundView;
private View mTimeIndicator;
private TextView mTimeText;
- private View mProgressEmptyBefore;
- private View mProgressWatched;
- private View mProgressBuffered;
- private View mProgressEmptyAfter;
- private View mControlBar;
+ private PlaybackProgressBar mProgress;
private PlayControlsButton mJumpPreviousButton;
private PlayControlsButton mRewindButton;
private PlayControlsButton mPlayPauseButton;
@@ -69,7 +64,6 @@ public class PlayControlsRowView extends MenuRowView {
private PlayControlsButton mRecordButton;
private TextView mProgramStartTimeText;
private TextView mProgramEndTimeText;
- private View mUnavailableMessageText;
private TunableTvView mTvView;
private TimeShiftManager mTimeShiftManager;
private final DvrDataManager mDvrDataManager;
@@ -83,6 +77,8 @@ public class PlayControlsRowView extends MenuRowView {
private final int mNormalButtonMargin;
private final int mCompactButtonMargin;
+ private final String mUnavailableMessage;
+
private final ScheduledRecordingListener mScheduledRecordingListener
= new ScheduledRecordingListener() {
@Override
@@ -138,6 +134,7 @@ public class PlayControlsRowView extends MenuRowView {
mDvrManager = null;
}
mMainActivity = (MainActivity) context;
+ mUnavailableMessage = res.getString(R.string.play_controls_unavailable);
}
@Override
@@ -171,14 +168,10 @@ public class PlayControlsRowView extends MenuRowView {
super.onFinishInflate();
// Clip the ViewGroup(body) to the rounded rectangle of outline.
findViewById(R.id.body).setClipToOutline(true);
- mBackgroundView = findViewById(R.id.background);
+ mBackgroundView = (TextView) findViewById(R.id.background);
mTimeIndicator = findViewById(R.id.time_indicator);
mTimeText = (TextView) findViewById(R.id.time_text);
- mProgressEmptyBefore = findViewById(R.id.timeline_bg_start);
- mProgressWatched = findViewById(R.id.watched);
- mProgressBuffered = findViewById(R.id.buffered);
- mProgressEmptyAfter = findViewById(R.id.timeline_bg_end);
- mControlBar = findViewById(R.id.play_control_bar);
+ mProgress = (PlaybackProgressBar) findViewById(R.id.progress);
mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous);
mRewindButton = (PlayControlsButton) findViewById(R.id.rewind);
mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause);
@@ -187,7 +180,6 @@ public class PlayControlsRowView extends MenuRowView {
mRecordButton = (PlayControlsButton) findViewById(R.id.record);
mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time);
mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time);
- mUnavailableMessageText = findViewById(R.id.unavailable_text);
initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous,
R.string.play_controls_description_skip_previous, null, new Runnable() {
@@ -195,7 +187,7 @@ public class PlayControlsRowView extends MenuRowView {
public void run() {
if (mTimeShiftManager.isAvailable()) {
mTimeShiftManager.jumpToPrevious();
- updateControls();
+ updateControls(true);
}
}
});
@@ -235,7 +227,7 @@ public class PlayControlsRowView extends MenuRowView {
public void run() {
if (mTimeShiftManager.isAvailable()) {
mTimeShiftManager.jumpToNext();
- updateControls();
+ updateControls(true);
}
}
});
@@ -265,18 +257,17 @@ public class PlayControlsRowView extends MenuRowView {
if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) {
Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel,
Toast.LENGTH_SHORT).show();
- } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity,
- currentChannel.getInputId())) {
+ } else {
Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager()
.getCurrentProgram(currentChannel.getId());
- if (program == null) {
- DvrUiHelper.showChannelRecordDurationOptions(mMainActivity, currentChannel);
- } else if (DvrUiHelper.handleCreateSchedule(mMainActivity, program)) {
- String msg = mMainActivity.getString(R.string.dvr_msg_current_program_scheduled,
- program.getTitle(),
- Utils.toTimeString(program.getEndTimeUtcMillis(), false));
- Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show();
- }
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity,
+ currentChannel.getInputId(), new Runnable() {
+ @Override
+ public void run() {
+ DvrUiHelper.requestRecordingCurrentProgram(mMainActivity,
+ currentChannel, program, true);
+ }
+ });
}
} else if (currentChannel != null) {
DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(),
@@ -318,39 +309,37 @@ public class PlayControlsRowView extends MenuRowView {
@Override
public void onAvailabilityChanged() {
updateMenuVisibility();
- if (isShown()) {
- PlayControlsRowView.this.updateAll();
- }
+ PlayControlsRowView.this.updateAll(false);
}
@Override
public void onPlayStatusChanged(int status) {
updateMenuVisibility();
- if (mTimeShiftManager.isAvailable() && isShown()) {
- updateControls();
+ if (mTimeShiftManager.isAvailable()) {
+ updateControls(false);
}
}
@Override
public void onRecordTimeRangeChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
- updateControls();
+ if (mTimeShiftManager.isAvailable()) {
+ updateControls(false);
}
}
@Override
public void onCurrentPositionChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
+ if (mTimeShiftManager.isAvailable()) {
initializeTimeline();
- updateControls();
+ updateControls(false);
}
}
@Override
public void onProgramInfoChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
+ if (mTimeShiftManager.isAvailable()) {
initializeTimeline();
- updateControls();
+ updateControls(false);
}
}
@@ -372,7 +361,8 @@ public class PlayControlsRowView extends MenuRowView {
}
}
});
- updateAll();
+ // force update to initialize everything
+ updateAll(true);
}
private void initializeTimeline() {
@@ -380,6 +370,8 @@ public class PlayControlsRowView extends MenuRowView {
mTimeShiftManager.getCurrentPositionMs());
mProgramStartTimeMs = program.getStartTimeUtcMillis();
mProgramEndTimeMs = program.getEndTimeUtcMillis();
+ mProgress.setMax(mProgramEndTimeMs - mProgramStartTimeMs);
+ updateRecTimeText();
SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs);
}
@@ -389,10 +381,13 @@ public class PlayControlsRowView extends MenuRowView {
getMenu().setKeepVisible(keepMenuVisible);
}
+ public void onPreselected() {
+ updateControls(true);
+ }
+
@Override
public void onSelected(boolean showTitle) {
super.onSelected(showTitle);
- updateControls();
postHideRippleAnimation();
}
@@ -474,28 +469,32 @@ public class PlayControlsRowView extends MenuRowView {
* Updates the view contents. It is called from the PlayControlsRow.
*/
public void update() {
- updateAll();
+ updateAll(false);
}
- private void updateAll() {
+ private void updateAll(boolean forceUpdate) {
if (mTimeShiftManager.isAvailable() && !mTvView.isScreenBlocked()) {
setEnabled(true);
initializeTimeline();
mBackgroundView.setEnabled(true);
+ setTextIfNeeded(mBackgroundView, null);
} else {
setEnabled(false);
mBackgroundView.setEnabled(false);
+ setTextIfNeeded(mBackgroundView, mUnavailableMessage);
}
- updateControls();
+ // force the controls be updated no matter it's visible or not.
+ updateControls(forceUpdate);
}
- private void updateControls() {
- updateTime();
- updateProgress();
- updateRecTimeText();
- updateButtons();
- updateRecordButton();
- updateButtonMargin();
+ private void updateControls(boolean forceUpdate) {
+ if (forceUpdate || getContentsView().isShown()) {
+ updateTime();
+ updateProgress();
+ updateButtons();
+ updateRecordButton();
+ updateButtonMargin();
+ }
}
private void updateTime() {
@@ -504,70 +503,39 @@ public class PlayControlsRowView extends MenuRowView {
mTimeIndicator.setVisibility(View.VISIBLE);
} else {
mTimeText.setVisibility(View.INVISIBLE);
- mTimeIndicator.setVisibility(View.INVISIBLE);
+ mTimeIndicator.setVisibility(View.GONE);
return;
}
long currentPositionMs = mTimeShiftManager.getCurrentPositionMs();
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams();
int currentTimePositionPixel =
convertDurationToPixel(currentPositionMs - mProgramStartTimeMs);
- params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin;
- mTimeText.setLayoutParams(params);
- mTimeText.setText(getTimeString(currentPositionMs));
- params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams();
- params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin;
- mTimeIndicator.setLayoutParams(params);
+ mTimeText.setTranslationX(currentTimePositionPixel + mTimeTextLeftMargin);
+ setTextIfNeeded(mTimeText, getTimeString(currentPositionMs));
+ mTimeIndicator.setTranslationX(currentTimePositionPixel + mTimeIndicatorLeftMargin);
}
private void updateProgress() {
if (isEnabled()) {
- mProgressWatched.setVisibility(View.VISIBLE);
- mProgressBuffered.setVisibility(View.VISIBLE);
- mProgressEmptyAfter.setVisibility(View.VISIBLE);
- } else {
- mProgressWatched.setVisibility(View.INVISIBLE);
- mProgressBuffered.setVisibility(View.INVISIBLE);
- mProgressEmptyAfter.setVisibility(View.INVISIBLE);
- if (mProgramStartTimeMs < mProgramEndTimeMs) {
- layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs);
- } else {
- // Not initialized yet.
- layoutProgress(mProgressEmptyBefore, mTimelineWidth);
- }
- return;
- }
-
- long progressStartTimeMs = Math.min(mProgramEndTimeMs,
+ long progressStartTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs()));
- long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
+ long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs()));
- long progressEndTimeMs = Math.min(mProgramEndTimeMs,
+ long progressEndTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs()));
-
- layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs);
- layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs);
- layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs);
- }
-
- private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) {
- layoutProgress(progress, Math.max(0,
- convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1);
- }
-
- private void layoutProgress(View progress, int width) {
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) progress.getLayoutParams();
- params.width = width;
- progress.setLayoutParams(params);
+ mProgress.setProgressRange(progressStartTimeMs - mProgramStartTimeMs,
+ progressEndTimeMs - mProgramStartTimeMs);
+ mProgress.setProgress(currentPlayingTimeMs - mProgramStartTimeMs);
+ } else {
+ mProgress.setProgressRange(0, 0);
+ }
}
private void updateRecTimeText() {
if (isEnabled()) {
mProgramStartTimeText.setVisibility(View.VISIBLE);
- mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs));
+ setTextIfNeeded(mProgramStartTimeText, getTimeString(mProgramStartTimeMs));
mProgramEndTimeText.setVisibility(View.VISIBLE);
- mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs));
+ setTextIfNeeded(mProgramEndTimeText, getTimeString(mProgramEndTimeMs));
} else {
mProgramStartTimeText.setVisibility(View.GONE);
mProgramEndTimeText.setVisibility(View.GONE);
@@ -576,11 +544,17 @@ public class PlayControlsRowView extends MenuRowView {
private void updateButtons() {
if (isEnabled()) {
- mControlBar.setVisibility(View.VISIBLE);
- mUnavailableMessageText.setVisibility(View.GONE);
+ mPlayPauseButton.setVisibility(View.VISIBLE);
+ mJumpPreviousButton.setVisibility(View.VISIBLE);
+ mJumpNextButton.setVisibility(View.VISIBLE);
+ mRewindButton.setVisibility(View.VISIBLE);
+ mFastForwardButton.setVisibility(View.VISIBLE);
} else {
- mControlBar.setVisibility(View.INVISIBLE);
- mUnavailableMessageText.setVisibility(View.VISIBLE);
+ mPlayPauseButton.setVisibility(View.GONE);
+ mJumpPreviousButton.setVisibility(View.GONE);
+ mJumpNextButton.setVisibility(View.GONE);
+ mRewindButton.setVisibility(View.GONE);
+ mFastForwardButton.setVisibility(View.GONE);
return;
}
@@ -622,6 +596,12 @@ public class PlayControlsRowView extends MenuRowView {
}
private void updateRecordButton() {
+ if (isEnabled()) {
+ mRecordButton.setVisibility(VISIBLE);
+ } else {
+ mRecordButton.setVisibility(GONE);
+ return;
+ }
if (!(mDvrManager != null
&& mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) {
mRecordButton.setVisibility(View.GONE);
@@ -682,4 +662,10 @@ public class PlayControlsRowView extends MenuRowView {
mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
}
}
+
+ private void setTextIfNeeded(TextView textView, String text) {
+ if (!TextUtils.equals(textView.getText(), text)) {
+ textView.setText(text);
+ }
+ }
}
diff --git a/src/com/android/tv/menu/PlaybackProgressBar.java b/src/com/android/tv/menu/PlaybackProgressBar.java
new file mode 100644
index 00000000..e8061bc6
--- /dev/null
+++ b/src/com/android/tv/menu/PlaybackProgressBar.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.menu;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.tv.R;
+
+/**
+ * A progress bar control which has two progresses which start in the middle of the control.
+ */
+public class PlaybackProgressBar extends View {
+ private final LayerDrawable mProgressDrawable;
+ private final Drawable mPrimaryDrawable;
+ private final Drawable mSecondaryDrawable;
+ private long mMax = 100;
+ private long mProgressStart = 0;
+ private long mProgressEnd = 0;
+ private long mProgress = 0;
+
+ public PlaybackProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes);
+ mProgressDrawable =
+ (LayerDrawable) a.getDrawable(R.styleable.PlaybackProgressBar_progressDrawable);
+ mPrimaryDrawable = mProgressDrawable.findDrawableByLayerId(android.R.id.progress);
+ mSecondaryDrawable =
+ mProgressDrawable.findDrawableByLayerId(android.R.id.secondaryProgress);
+ a.recycle();
+ refreshProgress();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mProgressDrawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ refreshProgress();
+ }
+
+ public void setMax(long max) {
+ if (max < 0) {
+ max = 0;
+ }
+ if (max != mMax) {
+ mMax = max;
+ if (mProgressStart > max) {
+ mProgressStart = max;
+ }
+ if (mProgressEnd > max) {
+ mProgressEnd = max;
+ }
+ if (mProgress > max) {
+ mProgress = max;
+ }
+ refreshProgress();
+ }
+ }
+
+ /**
+ * Sets the start and end position of the progress.
+ */
+ public void setProgressRange(long start, long end) {
+ start = constrain(start, 0, mMax);
+ end = constrain(end, start, mMax);
+ mProgress = constrain(mProgress, start, end);
+ if (start != mProgressStart || end != mProgressEnd) {
+ mProgressStart = start;
+ mProgressEnd = end;
+ setProgressLevels();
+ }
+ }
+
+ /**
+ * Sets the progress position.
+ */
+ public void setProgress(long progress) {
+ progress = constrain(progress, mProgressStart, mProgressEnd);
+ if (progress != mProgress) {
+ mProgress = progress;
+ setProgressLevels();
+ }
+ }
+
+ private long constrain(long value, long min, long max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ private void refreshProgress() {
+ int width = getWidth() - getPaddingStart() - getPaddingEnd();
+ int height = getHeight() - getPaddingTop() - getPaddingBottom();
+ mProgressDrawable.setBounds(0, 0, width, height);
+ setProgressLevels();
+ }
+
+ private void setProgressLevels() {
+ boolean progressUpdated = setProgressBound(mPrimaryDrawable, mProgressStart, mProgress);
+ progressUpdated |= setProgressBound(mSecondaryDrawable, mProgress, mProgressEnd);
+ if (progressUpdated) {
+ postInvalidate();
+ }
+ }
+
+ private boolean setProgressBound(Drawable drawable, long start, long end) {
+ Rect oldBounds = drawable.getBounds();
+ if (mMax == 0) {
+ if (!isEqualRect(oldBounds, 0, 0, 0, 0)) {
+ drawable.setBounds(0, 0, 0, 0);
+ return true;
+ }
+ return false;
+ }
+ int width = mProgressDrawable.getBounds().width();
+ int height = mProgressDrawable.getBounds().height();
+ int left = (int) (width * start / mMax);
+ int right = (int) (width * end / mMax);
+ if (!isEqualRect(oldBounds, left, 0, right, height)) {
+ drawable.setBounds(left, 0, right, height);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isEqualRect(Rect rect, int left, int top, int right, int bottom) {
+ return rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom;
+ }
+}
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index fb062246..220fcd3a 100644
--- a/src/com/android/tv/menu/TvOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/TvOptionsRowAdapter.java
@@ -21,7 +21,6 @@ import android.media.tv.TvTrackInfo;
import android.support.annotation.VisibleForTesting;
import com.android.tv.Features;
-import com.android.tv.R;
import com.android.tv.TvOptionsManager;
import com.android.tv.customization.CustomAction;
import com.android.tv.data.DisplayMode;
@@ -30,7 +29,6 @@ import com.android.tv.ui.sidepanel.ClosedCaptionFragment;
import com.android.tv.ui.sidepanel.DeveloperOptionFragment;
import com.android.tv.ui.sidepanel.DisplayModeFragment;
import com.android.tv.ui.sidepanel.MultiAudioFragment;
-import com.android.tv.util.PipInputManager;
import java.util.ArrayList;
import java.util.List;
@@ -39,12 +37,6 @@ import java.util.List;
* An adapter of options.
*/
public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
- private static final boolean ENABLE_IN_APP_PIP = false;
-
- private int mPositionPipAction;
- // If mInAppPipAction is false, system-wide PIP is used.
- private boolean mInAppPipAction = true;
-
public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) {
super(context, customActions);
}
@@ -53,123 +45,62 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
protected List<MenuAction> createBaseActions() {
List<MenuAction> actionList = new ArrayList<>();
actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
- setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- actionList.add(MenuAction.PIP_IN_APP_ACTION);
- setOptionChangedListener(MenuAction.PIP_IN_APP_ACTION);
- mPositionPipAction = actionList.size() - 1;
+ if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) {
+ actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION);
+ }
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
- setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
actionList.add(MenuAction.MORE_CHANNELS_ACTION);
if (DeveloperOptionFragment.shouldShow()) {
actionList.add(MenuAction.DEV_ACTION);
}
actionList.add(MenuAction.SETTINGS_ACTION);
- if (getCustomActions() != null) {
- // Adjust Pip action position which will be changed by applying custom actions.
- for (CustomAction customAction : getCustomActions()) {
- if (customAction.isFront()) {
- mPositionPipAction++;
- }
- }
- }
-
+ updateClosedCaptionAction();
+ updateMultiAudioAction();
+ updateDisplayModeAction();
return actionList;
}
@Override
- protected boolean updateActions() {
- boolean changed = false;
- if (updatePipAction()) {
- changed = true;
+ protected void updateActions() {
+ if (updateClosedCaptionAction()) {
+ notifyItemChanged(getItemPosition(MenuAction.SELECT_CLOSED_CAPTION_ACTION));
}
if (updateMultiAudioAction()) {
- changed = true;
+ notifyItemChanged(getItemPosition(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION));
}
if (updateDisplayModeAction()) {
- changed = true;
+ notifyItemChanged(getItemPosition(MenuAction.SELECT_DISPLAY_MODE_ACTION));
}
- return changed;
}
- private boolean updatePipAction() {
- // There are four states.
- // Case 1. The device doesn't even have any input for PIP. (e.g. OTT box without HDMI input)
- // => Remove the icon.
- // Case 2. The device has one or more inputs for PIP but none of them are currently
- // available.
- // => Show the icon but disable it.
- // Case 3. The device has one or more available PIP inputs and now it's tuned off.
- // => Show the icon with "Off".
- // Case 4. The device has one or more available PIP inputs but it's already turned on.
- // => Show the icon with "On".
-
- boolean changed = false;
-
- // Case 1
- PipInputManager pipInputManager = getMainActivity().getPipInputManager();
- if (ENABLE_IN_APP_PIP && pipInputManager.getPipInputSize(false) > 1) {
- if (!mInAppPipAction) {
- removeAction(mPositionPipAction);
- addAction(mPositionPipAction, MenuAction.PIP_IN_APP_ACTION);
- mInAppPipAction = true;
- changed = true;
- }
- } else {
- if (mInAppPipAction) {
- removeAction(mPositionPipAction);
- mInAppPipAction = false;
- if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) {
- addAction(mPositionPipAction, MenuAction.SYSTEMWIDE_PIP_ACTION);
- }
- return true;
- }
- return false;
- }
-
- // Case 2
- boolean isPipEnabled = getMainActivity().isPipEnabled();
- boolean oldEnabled = MenuAction.PIP_IN_APP_ACTION.isEnabled();
- boolean newEnabled = pipInputManager.getPipInputSize(true) > 0;
- if (oldEnabled != newEnabled) {
- // Should not disable the item if the PIP is already turned on so that the user can
- // force exit it.
- if (newEnabled || !isPipEnabled) {
- MenuAction.PIP_IN_APP_ACTION.setEnabled(newEnabled);
- changed = true;
- }
- }
-
- // Case 3 & 4 - we just need to update the icon.
- MenuAction.PIP_IN_APP_ACTION.setDrawableResId(
- isPipEnabled ? R.drawable.ic_tvoption_pip : R.drawable.ic_tvoption_pip_off);
- return changed;
+ @VisibleForTesting
+ private boolean updateClosedCaptionAction() {
+ return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
}
@VisibleForTesting
boolean updateMultiAudioAction() {
List<TvTrackInfo> audioTracks = getMainActivity().getTracks(TvTrackInfo.TYPE_AUDIO);
- boolean oldEnabled = MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled();
- boolean newEnabled = audioTracks != null && audioTracks.size() > 1;
- if (oldEnabled != newEnabled) {
- MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
+ boolean enabled = audioTracks != null && audioTracks.size() > 1;
+ // Use "|" operator for non-short-circuit evaluation.
+ return MenuAction.setEnabled(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION, enabled)
+ | updateActionDescription(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
}
private boolean updateDisplayModeAction() {
TvViewUiManager uiManager = getMainActivity().getTvViewUiManager();
- boolean oldEnabled = MenuAction.SELECT_DISPLAY_MODE_ACTION.isEnabled();
- boolean newEnabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL)
+ boolean enabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL)
|| uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM);
- if (oldEnabled != newEnabled) {
- MenuAction.SELECT_DISPLAY_MODE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
+ // Use "|" operator for non-short-circuit evaluation.
+ return MenuAction.setEnabled(MenuAction.SELECT_DISPLAY_MODE_ACTION, enabled)
+ | updateActionDescription(MenuAction.SELECT_DISPLAY_MODE_ACTION);
+ }
+
+ private boolean updateActionDescription(MenuAction action) {
+ return MenuAction.setActionDescription(action,
+ getMainActivity().getTvOptionsManager().getOptionString(action.getType()));
}
@Override
@@ -183,9 +114,6 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
getMainActivity().getOverlayManager().getSideFragmentManager()
.show(new DisplayModeFragment());
break;
- case TvOptionsManager.OPTION_IN_APP_PIP:
- getMainActivity().togglePipView();
- break;
case TvOptionsManager.OPTION_SYSTEMWIDE_PIP:
getMainActivity().enterPictureInPictureMode();
break;
@@ -205,4 +133,4 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
break;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/onboarding/SetupSourcesFragment.java b/src/com/android/tv/onboarding/SetupSourcesFragment.java
index 7607822c..f56daec5 100644
--- a/src/com/android/tv/onboarding/SetupSourcesFragment.java
+++ b/src/com/android/tv/onboarding/SetupSourcesFragment.java
@@ -38,6 +38,7 @@ import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.TvInputNewComparator;
+import com.android.tv.tuner.TunerInputController;
import com.android.tv.ui.GuidedActionsStylistWithDivider;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
@@ -204,6 +205,7 @@ public class SetupSourcesFragment extends SetupMultiPaneFragment {
mChannelDataManager.addListener(mChannelDataManagerListener);
super.onCreate(savedInstanceState);
mParentFragment = (SetupSourcesFragment) getParentFragment();
+ TunerInputController.executeNetworkTunerDiscoveryAsyncTask(getContext());
}
@Override
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index 8d6c5a14..03d7873f 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -27,7 +27,7 @@ import com.android.tv.Features;
import com.android.tv.TvActivity;
import com.android.tv.TvApplication;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.dvr.DvrRecordingService;
+import com.android.tv.dvr.recorder.DvrRecordingService;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java
index 8cd4fdf1..2d9ee10e 100644
--- a/src/com/android/tv/receiver/GlobalKeyReceiver.java
+++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java
@@ -20,6 +20,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.media.tv.TvContract;
+import android.os.AsyncTask;
+import android.provider.Settings;
import android.util.Log;
import android.view.KeyEvent;
@@ -31,27 +33,57 @@ import com.android.tv.TvApplication;
public class GlobalKeyReceiver extends BroadcastReceiver {
private static final boolean DEBUG = false;
private static final String TAG = "GlobalKeyReceiver";
+
private static final String ACTION_GLOBAL_BUTTON = "android.intent.action.GLOBAL_BUTTON";
+ // Settings.Secure.USER_SETUP_COMPLETE is hidden.
+ private static final String SETTINGS_USER_SETUP_COMPLETE = "user_setup_complete";
+
+ private static boolean sUserSetupComplete;
@Override
public void onReceive(Context context, Intent intent) {
TvApplication.setCurrentRunningProcess(context, true);
+ Context appContext = context.getApplicationContext();
+ if (DEBUG) Log.d(TAG, "onReceive: " + intent);
+ if (sUserSetupComplete) {
+ handleIntent(appContext, intent);
+ } else {
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return Settings.Secure.getInt(appContext.getContentResolver(),
+ SETTINGS_USER_SETUP_COMPLETE, 0) != 0;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean setupComplete) {
+ if (DEBUG) Log.d(TAG, "Is setup complete: " + setupComplete);
+ sUserSetupComplete = setupComplete;
+ if (sUserSetupComplete) {
+ handleIntent(appContext, intent);
+ }
+ }
+ }.execute();
+ }
+ }
+
+ private void handleIntent(Context appContext, Intent intent) {
if (ACTION_GLOBAL_BUTTON.equals(intent.getAction())) {
KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
- if (DEBUG) Log.d(TAG, "onReceive: " + event);
+ if (DEBUG) Log.d(TAG, "handleIntent: " + event);
int keyCode = event.getKeyCode();
int action = event.getAction();
if (action == KeyEvent.ACTION_UP) {
switch (keyCode) {
case KeyEvent.KEYCODE_GUIDE:
- context.startActivity(
+ appContext.startActivity(
new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI));
break;
case KeyEvent.KEYCODE_TV:
- ((TvApplication) context.getApplicationContext()).handleTvKey();
+ ((TvApplication) appContext).handleTvKey();
break;
case KeyEvent.KEYCODE_TV_INPUT:
- ((TvApplication) context.getApplicationContext()).handleTvInputKey();
+ ((TvApplication) appContext).handleTvInputKey();
break;
default:
// Do nothing
diff --git a/src/com/android/tv/receiver/PackageIntentsReceiver.java b/src/com/android/tv/receiver/PackageIntentsReceiver.java
index 26d000e7..2d3f8705 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -19,8 +19,10 @@ package com.android.tv.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import com.android.tv.TvApplication;
+import com.android.tv.util.Partner;
/**
* A class for handling the broadcast intents from PackageManager.
@@ -31,5 +33,9 @@ public class PackageIntentsReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
TvApplication.setCurrentRunningProcess(context, true);
((TvApplication) context.getApplicationContext()).handleInputCountChanged();
+
+ Uri uri = intent.getData();
+ final String packageName = (uri != null ? uri.getSchemeSpecificPart() : null);
+ Partner.reset(context, packageName);
}
}
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index 30ec73e3..a472f559 100644
--- a/src/com/android/tv/recommendation/NotificationService.java
+++ b/src/com/android/tv/recommendation/NotificationService.java
@@ -426,6 +426,7 @@ public class NotificationService extends Service implements Recommender.Listener
: 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
Intent intent = new Intent(Intent.ACTION_VIEW, channel.getUri());
intent.putExtra(TUNE_PARAMS_RECOMMENDATION_TYPE, mRecommendationType);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
final PendingIntent notificationIntent = PendingIntent.getActivity(this, 0, intent, 0);
// This callback will run on the main thread.
diff --git a/src/com/android/tv/search/DataManagerSearch.java b/src/com/android/tv/search/DataManagerSearch.java
index 5f89a21a..d90908f1 100644
--- a/src/com/android/tv/search/DataManagerSearch.java
+++ b/src/com/android/tv/search/DataManagerSearch.java
@@ -265,9 +265,7 @@ public class DataManagerSearch implements SearchInterface {
}
private String buildIntentData(long channelId) {
- return TvContract.buildChannelUri(channelId).buildUpon()
- .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH)
- .build().toString();
+ return TvContract.buildChannelUri(channelId).toString();
}
private boolean isRatingBlocked(TvContentRating[] ratings) {
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
index caa45812..c9a63128 100644
--- a/src/com/android/tv/search/SearchInterface.java
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -24,8 +24,6 @@ import java.util.List;
* Interface for channel and program search.
*/
public interface SearchInterface {
- String SOURCE_TV_SEARCH = "TvSearch";
-
int ACTION_TYPE_AMBIGUOUS = 1;
int ACTION_TYPE_SWITCH_CHANNEL = 2;
int ACTION_TYPE_SWITCH_INPUT = 3;
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 2ceec19a..ea144786 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -38,6 +38,8 @@ import com.android.tv.search.LocalSearchProvider.SearchResult;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.Utils;
+import junit.framework.Assert;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -189,6 +191,10 @@ public class TvProviderSearch implements SearchInterface {
@WorkerThread
private List<SearchResult> searchChannels(String query, String[] columnForExactMatching,
String[] columnForPartialMatching, Set<Long> channelsFound, int limit) {
+ Assert.assertTrue(
+ (columnForExactMatching != null && columnForExactMatching.length > 0) ||
+ (columnForPartialMatching != null && columnForPartialMatching.length > 0));
+
String[] projection = {
Channels._ID,
Channels.COLUMN_DISPLAY_NUMBER,
@@ -308,6 +314,10 @@ public class TvProviderSearch implements SearchInterface {
String[] columnForPartialMatching, Set<Long> channelsFound, int limit) {
if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'");
long time = SystemClock.elapsedRealtime();
+ Assert.assertTrue(
+ (columnForExactMatching != null && columnForExactMatching.length > 0) ||
+ (columnForPartialMatching != null && columnForPartialMatching.length > 0));
+
String[] projection = {
Programs.COLUMN_CHANNEL_ID,
Programs.COLUMN_TITLE,
@@ -402,9 +412,7 @@ public class TvProviderSearch implements SearchInterface {
}
private String buildIntentData(long channelId) {
- return TvContract.buildChannelUri(channelId).buildUpon()
- .appendQueryParameter(Utils.PARAM_SOURCE, SOURCE_TV_SEARCH)
- .build().toString();
+ return TvContract.buildChannelUri(channelId).toString();
}
private boolean isRatingBlocked(String ratings) {
diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java
index 2dd36074..8b06aaa9 100644
--- a/src/com/android/tv/tuner/ChannelScanFileParser.java
+++ b/src/com/android/tv/tuner/ChannelScanFileParser.java
@@ -18,7 +18,7 @@ package com.android.tv.tuner;
import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.Channel;
import java.io.BufferedReader;
import java.io.IOException;
diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java
index de19766e..64394ea3 100644
--- a/src/com/android/tv/tuner/TunerHal.java
+++ b/src/com/android/tv/tuner/TunerHal.java
@@ -20,6 +20,9 @@ import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.StringDef;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.Features;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,6 +51,7 @@ public abstract class TunerHal implements AutoCloseable {
public static final int TUNER_TYPE_BUILT_IN = 1;
public static final int TUNER_TYPE_USB = 2;
+ public static final int TUNER_TYPE_NETWORK = 3;
protected static final int PID_PAT = 0;
protected static final int PID_ATSC_SI_BASE = 0x1ffb;
@@ -69,31 +73,33 @@ public abstract class TunerHal implements AutoCloseable {
*/
public synchronized static TunerHal createInstance(Context context) {
TunerHal tunerHal = null;
- if (getTunerType(context) == TUNER_TYPE_BUILT_IN) {
+ if (useBuiltInTuner(context)) {
}
- if (tunerHal == null) {
+ if (tunerHal == null && UsbTunerHal.getNumberOfDevices(context) > 0) {
tunerHal = new UsbTunerHal(context);
}
- if (tunerHal.openFirstAvailable()) {
- return tunerHal;
- }
- return null;
+ return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
}
/**
* Gets the number of tuner devices currently present.
*/
- public static int getTunerCount(Context context) {
- if (getTunerType(context) == TUNER_TYPE_BUILT_IN) {
+ public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+ if (useBuiltInTuner(context)) {
}
- return UsbTunerHal.getNumberOfDevices(context);
+ int usbTunerCount = UsbTunerHal.getNumberOfDevices(context);
+ if (usbTunerCount > 0) {
+ return new Pair<>(TUNER_TYPE_USB, usbTunerCount);
+ }
+ return new Pair<>(null, 0);
}
/**
- * Gets the type of tuner devices currently used.
+ * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+ * tuners.
*/
- public static int getTunerType(Context context) {
- return TUNER_TYPE_USB;
+ static boolean useBuiltInTuner(Context context) {
+ return false;
}
protected TunerHal(Context context) {
@@ -106,6 +112,14 @@ public abstract class TunerHal implements AutoCloseable {
return mIsStreaming;
}
+ /**
+ * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels
+ * of the same frequency.
+ */
+ public boolean isReusable() {
+ return true;
+ }
+
@Override
protected void finalize() throws Throwable {
super.finalize();
@@ -131,9 +145,12 @@ public abstract class TunerHal implements AutoCloseable {
*
* @param frequency a frequency of the channel to tune to
* @param modulation a modulation method of the channel to tune to
+ * @param channelNumber channel number when channel number is already known. Some tuner HAL
+ * may use channelNumber instead of frequency for tune.
* @return {@code true} if the operation was successful, {@code false} otherwise
*/
- public synchronized boolean tune(int frequency, @ModulationType String modulation) {
+ public synchronized boolean tune(int frequency, @ModulationType String modulation,
+ String channelNumber) {
if (!isDeviceOpen()) {
Log.e(TAG, "There's no available device");
return false;
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
index d89b6a0c..65bbbdd0 100644
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ b/src/com/android/tv/tuner/TunerInputController.java
@@ -16,30 +16,40 @@
package com.android.tv.tuner;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.v4.os.BuildCompat;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.android.tv.Features;
+import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.tuner.R;
import com.android.tv.tuner.setup.TunerSetupActivity;
import com.android.tv.tuner.tvinput.TunerTvInputService;
+import com.android.tv.tuner.util.SystemPropertiesProxy;
import com.android.tv.tuner.util.TunerInputInfoUtils;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
/**
* Controls the package visibility of {@link TunerTvInputService}.
@@ -51,10 +61,39 @@ import java.util.Map;
public class TunerInputController extends BroadcastReceiver {
private static final boolean DEBUG = true;
private static final String TAG = "TunerInputController";
+ private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
+ private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
+ private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
+
+ /**
+ * Action of {@link Intent} to check network connection repeatedly when it is necessary.
+ */
+ public static final String CHECKING_NETWORK_CONNECTION =
+ "com.android.tv.action.CHECKING_NETWORK_CONNECTION";
+
+ /**
+ * Action of {@link Intent} when network tuner is attached.
+ */
+ public static final String NETWORK_TUNER_ATTACHED =
+ "com.android.tv.action.NETWORK_TUNER_ATTACHED";
+
+ /**
+ * Action of {@link Intent} when network tuner is detached.
+ */
+ public static final String NETWORK_TUNER_DETACHED =
+ "com.android.tv.action.NETWORK_TUNER_DETACHED";
+
+ private static final String EXTRA_CHECKING_DURATION =
+ "com.android.tv.action.extra.CHECKING_DURATION";
+
+ private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
private static final TunerDevice[] TUNER_DEVICES = {
- new TunerDevice(0x2040, 0xb123), // WinTV-HVR-955Q
- new TunerDevice(0x07ca, 0x0837) // AverTV Volar Hybrid Q
+ new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
+ new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
+ // WinTV-dualHD (bulk) will be supported after 2017 April security patch.
+ new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
};
private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
@@ -70,7 +109,9 @@ public class TunerInputController extends BroadcastReceiver {
if (mDvbDeviceAccessor == null) {
mDvbDeviceAccessor = new DvbDeviceAccessor(context);
}
- enableTunerTvInputService(context, mDvbDeviceAccessor.isDvbDeviceAvailable());
+ boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
+ enableTunerTvInputService(
+ context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null);
break;
}
}
@@ -84,14 +125,35 @@ public class TunerInputController extends BroadcastReceiver {
private final int vendorId;
private final int productId;
- private TunerDevice(int vendorId, int productId) {
+ // security patch level from which the specific tuner type is supported.
+ private final String minSecurityLevel;
+
+ private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
this.vendorId = vendorId;
this.productId = productId;
+ this.minSecurityLevel = minSecurityLevel;
}
private boolean equals(UsbDevice device) {
return device.getVendorId() == vendorId && device.getProductId() == productId;
}
+
+ private boolean isSupported(String currentSecurityLevel) {
+ if (minSecurityLevel == null) {
+ return true;
+ }
+
+ long supportSecurityLevelTimeStamp = 0;
+ long currentSecurityLevelTimestamp = 0;
+ try {
+ SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
+ supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
+ currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
+ } catch (ParseException e) {
+ }
+ return supportSecurityLevelTimeStamp != 0
+ && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
+ }
}
@Override
@@ -99,17 +161,20 @@ public class TunerInputController extends BroadcastReceiver {
if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
TvApplication.setCurrentRunningProcess(context, true);
if (!Features.TUNER.isEnabled(context)) {
- enableTunerTvInputService(context, false);
+ enableTunerTvInputService(context, false, false, null);
return;
}
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
switch (intent.getAction()) {
case Intent.ACTION_BOOT_COMPLETED:
+ executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS);
case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
case UsbManager.ACTION_USB_DEVICE_DETACHED:
- if (TunerInputInfoUtils.isBuiltInTuner(context)) {
- enableTunerTvInputService(context, true);
+ if (TunerHal.useBuiltInTuner(context)) {
+ enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN);
break;
}
// Falls back to the below to check USB tuner devices.
@@ -123,7 +188,41 @@ public class TunerInputController extends BroadcastReceiver {
mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
DVB_DRIVER_CHECK_DELAY_MS);
} else {
- enableTunerTvInputService(context, false);
+ if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
+ // Since network tuner is attached, do not disable TunerTvInput,
+ // just updates the TvInputInfo.
+ TunerInputInfoUtils.updateTunerInputInfo(context);
+ break;
+ }
+ enableTunerTvInputService(context, false, false, TextUtils
+ .equals(intent.getAction(), UsbManager.ACTION_USB_DEVICE_DETACHED) ?
+ TunerHal.TUNER_TYPE_USB : null);
+ }
+ break;
+ case CHECKING_NETWORK_CONNECTION:
+ long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION,
+ INITIAL_CHECKING_DURATION_MS);
+ executeNetworkTunerDiscoveryAsyncTask(context,
+ Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS));
+ break;
+ case NETWORK_TUNER_ATTACHED:
+ // Network tuner detection is initiated by UI. So the app should not
+ // be killed.
+ sharedPreferences.edit()
+ .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
+ enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK);
+ break;
+ case NETWORK_TUNER_DETACHED:
+ sharedPreferences.edit()
+ .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply();
+ if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) {
+ // Network tuner detection is initiated by UI. So the app should not
+ // be killed.
+ enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK);
+ } else {
+ // Since USB tuner is attached, do not disable TunerTvInput,
+ // just updates the TvInputInfo.
+ TunerInputInfoUtils.updateTunerInputInfo(context);
}
break;
}
@@ -138,12 +237,15 @@ public class TunerInputController extends BroadcastReceiver {
private boolean isUsbTunerConnected(Context context) {
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
Map<String, UsbDevice> deviceList = manager.getDeviceList();
+ String currentSecurityLevel =
+ SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
+
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equals(device)) {
+ if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
return true;
}
@@ -158,7 +260,8 @@ public class TunerInputController extends BroadcastReceiver {
* @param context {@link Context} instance
* @param enabled {@code true} to enable the service; otherwise {@code false}
*/
- private void enableTunerTvInputService(Context context, boolean enabled) {
+ private void enableTunerTvInputService(Context context, boolean enabled,
+ boolean forceDontKillApp, Integer tunerType) {
if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
PackageManager pm = context.getPackageManager();
ComponentName componentName = new ComponentName(context, TunerTvInputService.class);
@@ -170,7 +273,8 @@ public class TunerInputController extends BroadcastReceiver {
// Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds
// (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only
// when the LiveChannels app is active since we don't want to kill the running app.
- int flags = TvApplication.getSingletons(context).getMainActivityWrapper().isCreated()
+ int flags = forceDontKillApp
+ || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated()
? PackageManager.DONT_KILL_APP : 0;
int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
@@ -179,14 +283,67 @@ public class TunerInputController extends BroadcastReceiver {
TunerSetupActivity.onTvInputEnabled(context, enabled);
// Enable/disable the USB tuner TV input.
pm.setComponentEnabledSetting(componentName, newState, flags);
- if (!enabled) {
- Toast.makeText(
- context, R.string.msg_usb_device_detached, Toast.LENGTH_SHORT).show();
+ if (!enabled && tunerType != null) {
+ if (tunerType == TunerHal.TUNER_TYPE_USB) {
+ Toast.makeText(context, R.string.msg_usb_tuner_disconnected,
+ Toast.LENGTH_SHORT).show();
+ } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
+ Toast.makeText(context, R.string.msg_network_tuner_disconnected,
+ Toast.LENGTH_SHORT).show();
+ }
}
if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
} else if (enabled) {
- // When # of USB tuners is changed or the device just boots.
+ // When # of tuners is changed or the tuner input service is switching from/to using
+ // network tuners or the device just boots.
TunerInputInfoUtils.updateTunerInputInfo(context);
}
}
+
+ /**
+ * Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
+ */
+ public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
+ executeNetworkTunerDiscoveryAsyncTask(context, 0);
+ }
+
+ /**
+ * Discovers a network tuner.
+ * @param context {@link Context}
+ * @param repeatedDurationMs the time length to wait to repeatedly check network status to start
+ * finding network tuner when the network connection is not available.
+ * {@code 0} to disable repeatedly checking.
+ */
+ private static void executeNetworkTunerDiscoveryAsyncTask(final Context context,
+ final long repeatedDurationMs) {
+ if (!Features.NETWORK_TUNER.isEnabled(context)) {
+ return;
+ }
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isNetworkConnected(context)) {
+ // Implement and execute network tuner discovery AsyncTask here.
+ } else if (repeatedDurationMs > 0) {
+ AlarmManager alarmManager =
+ (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Intent networkCheckingIntent = new Intent(context, TunerInputController.class);
+ networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION);
+ networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs);
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(
+ context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime()
+ + repeatedDurationMs, alarmIntent);
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private static boolean isNetworkConnected(Context context) {
+ ConnectivityManager cm = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
}
diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java
index 1547e3ae..a387be74 100644
--- a/src/com/android/tv/tuner/TunerPreferences.java
+++ b/src/com/android/tv/tuner/TunerPreferences.java
@@ -39,6 +39,7 @@ public class TunerPreferences {
private static final String PREFS_KEY_CHANNEL_DATA_VERSION = "channel_data_version";
private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count";
+ private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code";
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";
@@ -86,8 +87,7 @@ public class TunerPreferences {
/**
* Releases the resources.
*/
- @MainThread
- public static void release(Context context) {
+ public static synchronized void release(Context context) {
if (useContentProvider(context) && sContentObserver != null) {
context.getContentResolver().unregisterContentObserver(sContentObserver);
}
@@ -99,7 +99,8 @@ public class TunerPreferences {
* This preferences is used across processes, so the preferences should be loaded again when the
* databases changes.
*/
- public static synchronized void loadPreferences(Context context) {
+ @MainThread
+ public static void loadPreferences(Context context) {
if (sLoadPreferencesTask != null
&& sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) {
sLoadPreferencesTask.cancel(true);
@@ -113,8 +114,7 @@ public class TunerPreferences {
return TisConfiguration.isPackagedWithLiveChannels(context);
}
- @MainThread
- public static int getChannelDataVersion(Context context) {
+ public static synchronized int getChannelDataVersion(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION,
@@ -126,8 +126,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setChannelDataVersion(Context context, int version) {
+ public static synchronized void setChannelDataVersion(Context context, int version) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version);
} else {
@@ -137,8 +136,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static int getScannedChannelCount(Context context) {
+ public static synchronized int getScannedChannelCount(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT);
@@ -148,8 +146,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setScannedChannelCount(Context context, int channelCount) {
+ public static synchronized void setScannedChannelCount(Context context, int channelCount) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount);
} else {
@@ -159,8 +156,25 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean isScanDone(Context context) {
+ public static synchronized String getLastPostalCode(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE);
+ } else {
+ return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null);
+ }
+ }
+
+ public static synchronized void setLastPostalCode(Context context, String postalCode) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode);
+ } else {
+ getSharedPreferences(context).edit()
+ .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode).apply();
+ }
+ }
+
+ public static synchronized boolean isScanDone(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_SCAN_DONE);
@@ -170,8 +184,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setScanDone(Context context) {
+ public static synchronized void setScanDone(Context context) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_SCAN_DONE, true);
} else {
@@ -181,8 +194,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean shouldShowSetupActivity(Context context) {
+ public static synchronized boolean shouldShowSetupActivity(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP);
@@ -192,8 +204,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setShouldShowSetupActivity(Context context, boolean need) {
+ public static synchronized void setShouldShowSetupActivity(Context context, boolean need) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_LAUNCH_SETUP, need);
} else {
@@ -203,8 +214,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean getStoreTsStream(Context context) {
+ public static synchronized boolean getStoreTsStream(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
@@ -214,8 +224,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setStoreTsStream(Context context, boolean shouldStore) {
+ public static synchronized void setStoreTsStream(Context context, boolean shouldStore) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
} else {
@@ -229,8 +238,23 @@ public class TunerPreferences {
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
}
- @MainThread
- private static void setPreference(final Context context, final String key, final String value) {
+ private static synchronized void setPreference(Context context, String key, String value) {
+ sPreferenceValues.putString(key, value);
+ savePreference(context, key, value);
+ }
+
+ private static synchronized void setPreference(Context context, String key, int value) {
+ sPreferenceValues.putInt(key, value);
+ savePreference(context, key, Integer.toString(value));
+ }
+
+ private static synchronized void setPreference(Context context, String key, boolean value) {
+ sPreferenceValues.putBoolean(key, value);
+ savePreference(context, key, Boolean.toString(value));
+ }
+
+ private static void savePreference(final Context context, final String key,
+ final String value) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -249,18 +273,6 @@ public class TunerPreferences {
}.execute();
}
- @MainThread
- private static void setPreference(Context context, String key, int value) {
- sPreferenceValues.putInt(key, value);
- setPreference(context, key, Integer.toString(value));
- }
-
- @MainThread
- private static void setPreference(Context context, String key, boolean value) {
- sPreferenceValues.putBoolean(key, value);
- setPreference(context, key, Boolean.toString(value));
- }
-
private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
private final Context mContext;
private LoadPreferencesTask(Context context) {
@@ -292,6 +304,9 @@ public class TunerPreferences {
case PREFS_KEY_STORE_TS_STREAM:
bundle.putBoolean(key, Boolean.parseBoolean(value));
break;
+ case PREFS_KEY_LAST_POSTAL_CODE:
+ bundle.putString(key, value);
+ break;
}
}
}
@@ -303,8 +318,10 @@ public class TunerPreferences {
}
@Override
- protected void onPostExecute(Bundle bundle) {
- sPreferenceValues.putAll(bundle);
+ protected synchronized void onPostExecute(Bundle bundle) {
+ if (bundle != null) {
+ sPreferenceValues.putAll(bundle);
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/UsbTunerHal.java b/src/com/android/tv/tuner/UsbTunerHal.java
index 22e35ea1..b1608ede 100644
--- a/src/com/android/tv/tuner/UsbTunerHal.java
+++ b/src/com/android/tv/tuner/UsbTunerHal.java
@@ -169,6 +169,10 @@ public class UsbTunerHal extends TunerHal {
* Gets the number of USB tuner devices currently present.
*/
public static int getNumberOfDevices(Context context) {
- return (new DvbDeviceAccessor(context)).getNumOfDvbDevices();
+ try {
+ return (new DvbDeviceAccessor(context)).getNumOfDvbDevices();
+ } catch (Exception e) {
+ return 0;
+ }
}
}
diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java
index a88538df..c41f1014 100644
--- a/src/com/android/tv/tuner/cc/CaptionLayout.java
+++ b/src/com/android/tv/tuner/cc/CaptionLayout.java
@@ -19,7 +19,7 @@ package com.android.tv.tuner.cc;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.layout.ScaledLayout;
/**
diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 3c75caa9..3aa40982 100644
--- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -27,7 +27,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java
index 92ab0620..c43fe512 100644
--- a/src/com/android/tv/tuner/cc/Cea708Parser.java
+++ b/src/com/android/tv/tuner/cc/Cea708Parser.java
@@ -140,6 +140,7 @@ public class Cea708Parser {
private int mCommand = 0;
private int mListenServiceNumber = 0;
private boolean mDtvCcPacking = false;
+ private boolean mFirstServiceNumberDiscovered;
// Assign a dummy listener in order to avoid null checks.
private OnCea708ParserListener mListener = new OnCea708ParserListener() {
@@ -332,12 +333,14 @@ public class Cea708Parser {
mDiscoveredNumBytes.put(
serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0));
}
- if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()) {
+ if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()
+ || !mFirstServiceNumberDiscovered) {
for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) {
int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i);
if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) {
int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i);
mListener.discoverServiceNumber(discoveredServiceNumber);
+ mFirstServiceNumberDiscovered = true;
}
}
mDiscoveredNumBytes.clear();
diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java
index 67700c6a..2c8a52db 100644
--- a/src/com/android/tv/tuner/data/PsiData.java
+++ b/src/com/android/tv/tuner/data/PsiData.java
@@ -17,8 +17,8 @@
package com.android.tv.tuner.data;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import java.util.List;
diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java
index aead4be8..ac7fdedb 100644
--- a/src/com/android/tv/tuner/data/PsipData.java
+++ b/src/com/android/tv/tuner/data/PsipData.java
@@ -20,11 +20,11 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.text.format.DateUtils;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.ts.SectionParser;
import com.android.tv.tuner.util.ConvertUtils;
-import com.android.tv.tuner.util.StringUtils;
+import com.android.tv.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java
index 89079d77..41f66e7d 100644
--- a/src/com/android/tv/tuner/data/TunerChannel.java
+++ b/src/com/android/tv/tuner/data/TunerChannel.java
@@ -19,12 +19,11 @@ package com.android.tv.tuner.data;
import android.support.annotation.NonNull;
import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Channel.TunerChannelProto;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Channel.TunerChannelProto;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.util.Ints;
-import com.android.tv.tuner.util.StringUtils;
+import com.android.tv.util.StringUtils;
import com.google.protobuf.nano.MessageNano;
import java.io.IOException;
@@ -40,6 +39,11 @@ import java.util.Objects;
public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracksInterface {
private static final String TAG = "TunerChannel";
+ /**
+ * Channel number separator between major number and minor number.
+ */
+ public static final char CHANNEL_NUMBER_SEPARATOR = '-';
+
// See ATSC Code Points Registry.
private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] {
"ATSC Reserved",
@@ -63,6 +67,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
// According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff.
public static final int INVALID_STREAMTYPE = -1;
+ // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766
private final TunerChannelProto mProto;
private TunerChannel(PsipData.VctItem channel, int programNumber,
@@ -145,6 +150,44 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE);
}
+ /**
+ * Create a TunerChannel object suitable for network tuners
+ * @param major Channel number major
+ * @param minor Channel number minor
+ * @param programNumber Program number
+ * @param shortName Short name
+ * @param recordingProhibited Recording prohibition info
+ * @param videoFormat Video format. Should be {@code null} or one of the followings:
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P}
+ * @return a TunerChannel object
+ */
+ public static TunerChannel forNetwork(int major, int minor, int programNumber,
+ String shortName, boolean recordingProhibited, String videoFormat) {
+ TunerChannel tunerChannel = new TunerChannel(programNumber, Collections.EMPTY_LIST);
+ tunerChannel.setVirtualMajor(major);
+ tunerChannel.setVirtualMinor(minor);
+ tunerChannel.setShortName(shortName);
+ // Set audio and video pids in order to work around the audio-only channel check.
+ tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0)));
+ tunerChannel.selectAudioTrack(0);
+ tunerChannel.setVideoPid(0);
+ tunerChannel.setRecordingProhibited(recordingProhibited);
+ if (videoFormat != null) {
+ tunerChannel.setVideoFormat(videoFormat);
+ }
+ return tunerChannel;
+ }
+
public String getName() {
return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName;
}
@@ -193,7 +236,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.videoPid;
}
- public void setVideoPid(int videoPid) {
+ synchronized public void setVideoPid(int videoPid) {
mProto.videoPid = videoPid;
}
@@ -219,7 +262,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioPids);
}
- public void setAudioPids(List<Integer> audioPids) {
+ synchronized public void setAudioPids(List<Integer> audioPids) {
mProto.audioPids = Ints.toArray(audioPids);
}
@@ -227,7 +270,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioStreamTypes);
}
- public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
+ synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
}
@@ -239,32 +282,32 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.type;
}
- public void setFilepath(String filepath) {
- mProto.filepath = filepath;
+ synchronized public void setFilepath(String filepath) {
+ mProto.filepath = filepath == null ? "" : filepath;
}
public String getFilepath() {
return mProto.filepath;
}
- public void setVirtualMajor(int virtualMajor) {
+ synchronized public void setVirtualMajor(int virtualMajor) {
mProto.virtualMajor = virtualMajor;
}
- public void setVirtualMinor(int virtualMinor) {
+ synchronized public void setVirtualMinor(int virtualMinor) {
mProto.virtualMinor = virtualMinor;
}
- public void setShortName(String shortName) {
- mProto.shortName = shortName;
+ synchronized public void setShortName(String shortName) {
+ mProto.shortName = shortName == null ? "" : shortName;
}
- public void setFrequency(int frequency) {
+ synchronized public void setFrequency(int frequency) {
mProto.frequency = frequency;
}
- public void setModulation(String modulation) {
- mProto.modulation = modulation;
+ synchronized public void setModulation(String modulation) {
+ mProto.modulation = modulation == null ? "" : modulation;
}
public boolean hasVideo() {
@@ -279,13 +322,18 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.channelId;
}
- public void setChannelId(long channelId) {
+ synchronized public void setChannelId(long channelId) {
mProto.channelId = channelId;
}
public String getDisplayNumber() {
- if (mProto.virtualMajor != 0 && mProto.virtualMinor != 0) {
- return String.format("%d-%d", mProto.virtualMajor, mProto.virtualMinor);
+ return getDisplayNumber(true);
+ }
+
+ public String getDisplayNumber(boolean ignoreZeroMinorNumber) {
+ if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) {
+ return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR,
+ mProto.virtualMinor);
} else if (mProto.virtualMajor != 0) {
return Integer.toString(mProto.virtualMajor);
} else {
@@ -298,7 +346,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
@Override
- public void setHasCaptionTrack() {
+ synchronized public void setHasCaptionTrack() {
mProto.hasCaptionTrack = true;
}
@@ -312,7 +360,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks));
}
- public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
+ synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]);
}
@@ -321,11 +369,11 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks));
}
- public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]);
}
- public void selectAudioTrack(int index) {
+ synchronized public void selectAudioTrack(int index) {
if (0 <= index && index < mProto.audioPids.length) {
mProto.audioTrackIndex = index;
} else {
@@ -333,6 +381,22 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
}
+ synchronized public void setRecordingProhibited(boolean recordingProhibited) {
+ mProto.recordingProhibited = recordingProhibited;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mProto.recordingProhibited;
+ }
+
+ synchronized public void setVideoFormat(String videoFormat) {
+ mProto.videoFormat = videoFormat == null ? "" : videoFormat;
+ }
+
+ public String getVideoFormat() {
+ return mProto.videoFormat;
+ }
+
@Override
public String toString() {
switch (mProto.type) {
@@ -359,7 +423,10 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
if (ret != 0) {
return ret;
}
-
+ ret = StringUtils.compare(getName(), channel.getName());
+ if (ret != 0) {
+ return ret;
+ }
// For FileTsStreamer, file paths should be compared.
return StringUtils.compare(getFilepath(), channel.getFilepath());
}
@@ -374,12 +441,19 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
@Override
public int hashCode() {
- return Objects.hash(getFrequency(), getProgramNumber(), getFilepath());
+ return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath());
}
// Serialization
- public byte[] toByteArray() {
- return MessageNano.toByteArray(mProto);
+ synchronized public byte[] toByteArray() {
+ try {
+ return MessageNano.toByteArray(mProto);
+ } catch (Exception e) {
+ // Retry toByteArray. b/34197766
+ Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock",
+ e);
+ return MessageNano.toByteArray(mProto);
+ }
}
public static TunerChannel parseFrom(byte[] data) {
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index c105e222..89641530 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,17 +23,29 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.util.Pair;
-import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
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;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
@@ -42,10 +54,11 @@ import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
/**
* A class that extracts samples from a live broadcast stream while storing the sample on the disk.
@@ -54,11 +67,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class ExoPlayerSampleExtractor implements SampleExtractor {
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 int INVALID_TRACK_INDEX = -1;
private final HandlerThread mSourceReaderThread;
private final long mId;
@@ -70,36 +79,69 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
private IOException mExceptionOnPrepare;
private List<MediaFormat> mTrackFormats;
+ private int mVideoTrackIndex = INVALID_TRACK_INDEX;
+ private boolean mVideoTrackMet;
+ private long mBaseSamplePts = Long.MIN_VALUE;
private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
+ private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>();
private OnCompletionListener mOnCompletionListener;
private Handler mOnCompletionListenerHandler;
private IOException mError;
- public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
+ public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager,
PlaybackBufferListener bufferListener, boolean isRecording) {
// It'll be used as a timeshift file chunk name's prefix.
mId = System.currentTimeMillis();
- Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
EventListener eventListener = new EventListener() {
-
@Override
- public void onLoadError(int sourceId, IOException e) {
- mError = e;
+ public void onLoadError(IOException error) {
+ mError = error;
}
};
mSourceReaderThread = new HandlerThread("SourceReaderThread");
- mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
- allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
+ mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri,
+ new com.google.android.exoplayer2.upstream.DataSource.Factory() {
+ @Override
+ public com.google.android.exoplayer2.upstream.DataSource createDataSource() {
+ // Returns an adapter implementation for ExoPlayer V2 DataSource interface.
+ return new com.google.android.exoplayer2.upstream.DataSource() {
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ return source.open(
+ new com.google.android.exoplayer.upstream.DataSpec(
+ dataSpec.uri, dataSpec.postBody,
+ dataSpec.absoluteStreamPosition, dataSpec.position,
+ dataSpec.length, dataSpec.key, dataSpec.flags));
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength)
+ throws IOException {
+ return source.read(buffer, offset, readLength);
+ }
+
+ @Override
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+ };
+ }
+ },
+ new DefaultExtractorsFactory(),
// Do not create a handler if we not on a looper. e.g. test.
- Looper.myLooper() != null ? new Handler() : null,
- eventListener, 0));
+ Looper.myLooper() != null ? new Handler() : null, eventListener));
if (isRecording) {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
RecordingSampleBuffer.BUFFER_REASON_RECORDING);
} else {
- if (bufferManager == null || bufferManager.isDisabled()) {
+ if (bufferManager == null) {
mSampleBuffer = new SimpleSampleBuffer(bufferListener);
} else {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true,
@@ -114,43 +156,141 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mOnCompletionListenerHandler = handler;
}
- private class SourceReaderWorker implements Handler.Callback {
+ private class SourceReaderWorker implements Handler.Callback, MediaPeriod.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 final MediaSource mSampleSource;
+ private MediaPeriod mMediaPeriod;
+ private SampleStream[] mStreams;
private boolean[] mTrackMetEos;
private boolean mMetEos = false;
private long mCurrentPosition;
+ private DecoderInputBuffer mDecoderInputBuffer;
+ private SampleHolder mSampleHolder;
+ private boolean mPrepareRequested;
- public SourceReaderWorker(SampleSource sampleSource) {
+ public SourceReaderWorker(MediaSource sampleSource) {
mSampleSource = sampleSource;
+ mSampleSource.prepareSource(null, false, new MediaSource.Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ // Dynamic stream change is not supported yet. b/28169263
+ // For now, this will cause EOS and playback reset.
+ }
+ });
+ mDecoderInputBuffer = new DecoderInputBuffer(
+ DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ MediaFormat convertFormat(Format format) {
+ if (format.sampleMimeType.startsWith("audio/")) {
+ return MediaFormat.createAudioFormat(format.id, format.sampleMimeType,
+ format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount,
+ format.sampleRate, format.initializationData, format.language,
+ format.pcmEncoding);
+ } else if (format.sampleMimeType.startsWith("video/")) {
+ return MediaFormat.createVideoFormat(
+ format.id, format.sampleMimeType, format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height,
+ format.initializationData, format.rotationDegrees,
+ format.pixelWidthHeightRatio, format.projectionData, format.stereoMode);
+ } else if (format.sampleMimeType.endsWith("/cea-608")
+ || format.sampleMimeType.startsWith("text/")) {
+ return MediaFormat.createTextFormat(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language);
+ } else {
+ return MediaFormat.createFormatForMimeType(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US);
+ }
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ if (mMediaPeriod == null) {
+ // This instance is already released while the extractor is preparing.
+ return;
+ }
+ TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory();
+ TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups();
+ TrackSelection[] selections = new TrackSelection[trackGroupArray.length];
+ for (int i = 0; i < selections.length; ++i) {
+ selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0);
+ }
+ boolean retain[] = new boolean[trackGroupArray.length];
+ boolean reset[] = new boolean[trackGroupArray.length];
+ mStreams = new SampleStream[trackGroupArray.length];
+ mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0);
+ if (mTrackFormats == null) {
+ int trackCount = trackGroupArray.length;
+ mTrackMetEos = new boolean[trackCount];
+ List<MediaFormat> trackFormats = new ArrayList<>();
+ int videoTrackCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ Format format = trackGroupArray.get(i).getFormat(0);
+ if (format.sampleMimeType.startsWith("video/")) {
+ videoTrackCount++;
+ mVideoTrackIndex = i;
+ }
+ trackFormats.add(convertFormat(format));
+ }
+ if (videoTrackCount > 1) {
+ // Disable dropping samples when there are multiple video tracks.
+ mVideoTrackIndex = INVALID_TRACK_INDEX;
+ }
+ 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;
+ }
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ mPrepared = true;
+ }
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ source.continueLoading(mCurrentPosition);
}
@Override
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 (!mPrepareRequested) {
+ mPrepareRequested = true;
+ mMediaPeriod = mSampleSource.createPeriod(0,
+ new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0);
+ mMediaPeriod.prepare(this);
+ try {
+ mMediaPeriod.maybeThrowPrepareError();
+ } catch (IOException e) {
+ mError = 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();
+ int trackCount = mStreams.length;
for (int i = 0; i < trackCount; ++i) {
- if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
- != fetchSample(i, sample, conditionVariable)) {
+ if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ
+ != fetchSample(i, mSampleHolder, conditionVariable)) {
if (mMetEos) {
// If mMetEos was on during fetchSample() due to an error,
// fetching from other tracks is not necessary.
@@ -159,6 +299,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
didSomething = true;
}
}
+ mMediaPeriod.continueLoading(mCurrentPosition);
if (!mMetEos) {
if (didSomething) {
mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
@@ -171,17 +312,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
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;
+ if (mMediaPeriod != null) {
+ mSampleSource.releasePeriod(mMediaPeriod);
+ mSampleSource.releaseSource();
+ mMediaPeriod = null;
}
cleanUp();
mSourceReaderHandler.removeCallbacksAndMessages(null);
@@ -190,91 +324,109 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
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;
- }
- }
- return true;
- }
-
private int fetchSample(int track, SampleHolder sample,
ConditionVariable conditionVariable) {
- mSampleSourceReader.continueBuffering(track, mCurrentPosition);
-
- MediaFormatHolder formatHolder = new MediaFormatHolder();
- sample.clearData();
- int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample);
- if (ret == SampleSource.SAMPLE_READ) {
- if (mCurrentPosition < sample.timeUs) {
- mCurrentPosition = sample.timeUs;
+ FormatHolder dummyFormatHolder = new FormatHolder();
+ mDecoderInputBuffer.clear();
+ int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer);
+ if (ret == C.RESULT_BUFFER_READ
+ // Double-check if the extractor provided the data to prevent NPE. b/33758354
+ && mDecoderInputBuffer.data != null) {
+ if (mCurrentPosition < mDecoderInputBuffer.timeUs) {
+ mCurrentPosition = mDecoderInputBuffer.timeUs;
}
try {
Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track);
if (lastExtractedPositionUs == null) {
- mLastExtractedPositionUsMap.put(track, sample.timeUs);
+ mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs);
} else {
mLastExtractedPositionUsMap.put(track,
- Math.max(lastExtractedPositionUs, sample.timeUs));
+ Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs));
}
- queueSample(track, sample, conditionVariable);
+ queueSample(track, conditionVariable);
} catch (IOException e) {
mLastExtractedPositionUsMap.clear();
mMetEos = true;
mSampleBuffer.setEos();
}
- } else if (ret == SampleSource.END_OF_STREAM) {
+ } else if (ret == C.RESULT_END_OF_INPUT) {
mTrackMetEos[track] = true;
for (int i = 0; i < mTrackMetEos.length; ++i) {
if (!mTrackMetEos[i]) {
break;
}
- if (i == mTrackMetEos.length -1) {
+ if (i == mTrackMetEos.length - 1) {
mMetEos = true;
mSampleBuffer.setEos();
}
}
}
- // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
+ // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263
return ret;
}
- }
-
- 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();
+ private void queueSample(int index, ConditionVariable conditionVariable)
+ throws IOException {
+ if (mVideoTrackIndex != INVALID_TRACK_INDEX) {
+ if (!mVideoTrackMet) {
+ if (index != mVideoTrackIndex) {
+ SampleHolder sample =
+ new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY
+ : 0);
+ sample.timeUs = mDecoderInputBuffer.timeUs;
+ sample.size = mDecoderInputBuffer.data.position();
+ sample.ensureSpaceForWrite(sample.size);
+ mDecoderInputBuffer.flip();
+ sample.data.position(0);
+ sample.data.put(mDecoderInputBuffer.data);
+ sample.data.flip();
+ mPendingSamples.add(new Pair<>(index, sample));
+ return;
+ }
+ mVideoTrackMet = true;
+ mBaseSamplePts =
+ mDecoderInputBuffer.timeUs
+ - Ac3DefaultTrackRenderer.INITIAL_AUDIO_BUFFERING_TIME_US;
+ for (Pair<Integer, SampleHolder> pair : mPendingSamples) {
+ if (pair.second.timeUs >= mBaseSamplePts) {
+ mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable);
+ }
+ }
+ mPendingSamples.clear();
+ } else {
+ if (mDecoderInputBuffer.timeUs < mBaseSamplePts
+ && mVideoTrackIndex != index) {
+ return;
+ }
+ }
+ }
+ // Copy the decoder input to the sample holder.
+ mSampleHolder.clearData();
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0);
+ mSampleHolder.timeUs = mDecoderInputBuffer.timeUs;
+ mSampleHolder.size = mDecoderInputBuffer.data.position();
+ mSampleHolder.ensureSpaceForWrite(mSampleHolder.size);
+ mDecoderInputBuffer.flip();
+ mSampleHolder.data.position(0);
+ mSampleHolder.data.put(mDecoderInputBuffer.data);
+ mSampleHolder.data.flip();
+ long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
+ mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable);
+
+ // Checks whether the storage has enough bandwidth for recording samples.
+ if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size,
+ SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
+ mSampleBuffer.handleWriteSpeedSlow();
+ }
}
}
@@ -328,7 +480,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
- public boolean continueBuffering(long positionUs) {
+ public boolean continueBuffering(long positionUs) {
return mSampleBuffer.continueBuffering(positionUs);
}
@@ -386,12 +538,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
private long getLastExtractedPositionUs() {
- long lastExtractedPositionUs = Long.MAX_VALUE;
- for (long value : mLastExtractedPositionUsMap.values()) {
- lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
+ long lastExtractedPositionUs = Long.MIN_VALUE;
+ for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) {
+ if (mVideoTrackIndex != entry.getKey()) {
+ lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue());
+ }
}
- if (lastExtractedPositionUs == Long.MAX_VALUE) {
- lastExtractedPositionUs = C.UNKNOWN_TIME_US;
+ if (lastExtractedPositionUs == Long.MIN_VALUE) {
+ lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US;
}
return lastExtractedPositionUs;
}
diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index ec7b4b16..b7e42a7c 100644
--- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -25,7 +25,6 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import android.os.Handler;
-import android.util.Pair;
import java.io.IOException;
import java.util.ArrayList;
@@ -61,18 +60,17 @@ public class FileSampleExtractor implements SampleExtractor{
@Override
public boolean prepare() throws IOException {
- ArrayList<Pair<String, android.media.MediaFormat>> trackInfos =
- mBufferManager.readTrackInfoFiles();
- if (trackInfos == null || trackInfos.isEmpty()) {
+ List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles();
+ if (trackFormatList == null || trackFormatList.isEmpty()) {
throw new IOException("Cannot find meta files for the recording.");
}
- mTrackCount = trackInfos.size();
+ mTrackCount = trackFormatList.size();
List<String> ids = new ArrayList<>();
mTrackFormats.clear();
for (int i = 0; i < mTrackCount; ++i) {
- Pair<String, android.media.MediaFormat> pair = trackInfos.get(i);
- ids.add(pair.first);
- mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second));
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(i);
+ ids.add(trackFormat.trackId);
+ mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format));
}
mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true,
RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index 381b22e9..ba0edf20 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -39,8 +39,8 @@ 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.exoplayer.ac3.Ac3DefaultTrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3MediaCodecTrackRenderer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -48,11 +48,12 @@ import com.android.tv.tuner.tvinput.EventDetector;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-/**
- * MPEG-2 TS stream player implementation using ExoPlayer.
- */
-public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener,
- Ac3PassthroughTrackRenderer.EventListener, Ac3TrackRenderer.Ac3EventListener {
+/** MPEG-2 TS stream player implementation using ExoPlayer. */
+public class MpegTsPlayer
+ implements ExoPlayer.Listener,
+ MediaCodecVideoTrackRenderer.EventListener,
+ Ac3DefaultTrackRenderer.EventListener,
+ Ac3MediaCodecTrackRenderer.Ac3EventListener {
private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER;
/**
@@ -304,8 +305,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed()));
mPlayer.setPlayWhenReady(true);
mTrickplayRunning = true;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED,
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED,
playbackParams.getSpeed());
} else {
mPlayer.sendMessage(mAudioRenderer,
@@ -317,10 +320,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
private void stopSmoothTrickplay(boolean calledBySeek) {
if (mTrickplayRunning) {
mTrickplayRunning = false;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer,
- Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED,
- 1.0f);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f);
} else {
mPlayer.sendMessage(mAudioRenderer,
MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS,
@@ -423,8 +425,8 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
*/
public void setVolume(float volume) {
mVolume = volume;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_VOLUME, volume);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_VOLUME, volume);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
volume);
@@ -437,9 +439,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* @param enable enables the audio when {@code true}, disables otherwise.
*/
public void setAudioTrack(boolean enable) {
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK,
- enable ? 1 : 0);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
enable ? mVolume : 0.0f);
@@ -495,6 +497,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
+ * Returns the index of the currently selected track for the specified renderer.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @return The selected track. A negative value or a value greater than or equal to the renderer's
+ * track count indicates that the renderer is disabled.
+ */
+ public int getSelectedTrack(int rendererIndex) {
+ return mPlayer.getSelectedTrack(rendererIndex);
+ }
+
+ /**
+ * Returns the format of a track.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @param trackIndex The index of the track.
+ * @return The format of the track.
+ */
+ public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) {
+ return mPlayer.getTrackFormat(rendererIndex, trackIndex);
+ }
+
+ /**
* Gets the main handler of the player.
*/
/* package */ Handler getMainHandler() {
@@ -650,4 +674,4 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index 0e46c9cf..a1a97d3d 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -21,9 +21,10 @@ import android.content.Context;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
+import com.android.tv.Features;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
-import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
@@ -52,10 +53,12 @@ public class MpegTsRendererBuilder implements RendererBuilder {
SampleSource sampleSource = new MpegTsSampleSource(extractor);
MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext,
sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer);
- // TODO: Only using Ac3PassthroughTrackRenderer for A/V sync issue. We will use
- // {@link Ac3TrackRenderer} when we use ExoPlayer's extractor.
- TrackRenderer audioRenderer = new Ac3PassthroughTrackRenderer(sampleSource,
- mpegTsPlayer.getMainHandler(), mpegTsPlayer);
+ // TODO: Only using Ac3DefaultTrackRenderer for A/V sync issue. We will use
+ // {@link Ac3MediaCodecTrackRenderer} when we use ExoPlayer's extractor.
+ TrackRenderer audioRenderer =
+ new Ac3DefaultTrackRenderer(
+ sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer,
+ !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext));
Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource);
TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT];
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
index 9dae2e34..d442fde8 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
@@ -23,16 +23,15 @@ import android.util.Log;
import com.google.android.exoplayer.CodecCounters;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaClock;
-import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
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.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
@@ -40,9 +39,9 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
- * Decodes and renders AC3 audio.
+ * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding.
*/
-public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock {
+public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock {
public static final int MSG_SET_VOLUME = 10000;
public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1;
public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2;
@@ -51,7 +50,14 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
// One AC3 sample has 1536 frames, and its duration is 32ms.
public static final long AC3_SAMPLE_DURATION_US = 32000;
- private static final String TAG = "Ac3PassthroughTrackRenderer";
+ // This is around 150ms, 150ms is big enough not to under-run AudioTrack,
+ // and 150ms is also small enough to fill the buffer rapidly.
+ static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5;
+ public static final long INITIAL_AUDIO_BUFFERING_TIME_US =
+ BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US;
+
+
+ private static final String TAG = "Ac3DefaultTrackRenderer";
private static final boolean DEBUG = false;
/**
@@ -93,6 +99,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private final AudioClock mAudioClock;
private MediaFormat mFormat;
+ private boolean mFormatConfigured;
+ private int mSampleSize;
private final ByteBuffer mOutputBuffer;
private boolean mOutputReady;
private int mTrackIndex;
@@ -106,10 +114,15 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private long mInterpolatedTimeUs;
private long mPreviousPositionUs;
private boolean mIsStopped;
+ private boolean mEnabled = true;
+ private boolean mIsMuted;
private ArrayList<Integer> mTracksIndex;
- public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler,
- EventListener listener) {
+ public Ac3DefaultTrackRenderer(
+ SampleSource source,
+ Handler eventHandler,
+ EventListener listener,
+ boolean usePassthrough) {
mSource = source.register();
mEventHandler = eventHandler;
mEventListener = listener;
@@ -325,14 +338,38 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
}
}
+ private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
+ return MediaFormat.createAudioFormat(
+ format.trackId,
+ MimeTypes.AUDIO_RAW,
+ format.bitrate,
+ format.maxInputSize,
+ format.durationUs,
+ format.channelCount,
+ format.sampleRate,
+ format.initializationData,
+ format.language);
+ }
+
private void onInputFormatChanged(MediaFormatHolder formatHolder)
throws ExoPlaybackException {
- mFormat = formatHolder.format;
+ mFormat = formatHolder.format;
+ mFormatConfigured = true;
if (DEBUG) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString());
}
clearDecodeState();
- AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16());
+ AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0);
+ }
+
+ private void onSampleSizeChanged(int sampleSize) {
+ if (DEBUG) {
+ Log.d(TAG, "Sample size was changed to : " + sampleSize);
+ }
+ clearDecodeState();
+ int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK;
+ mSampleSize = sampleSize;
+ AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize);
}
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
@@ -359,8 +396,11 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
default: {
+ if (mSampleHolder.size != mSampleSize && mFormatConfigured) {
+ onSampleSizeChanged(mSampleHolder.size);
+ }
mSampleHolder.data.flip();
- decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
+ decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
return true;
}
}
@@ -511,24 +551,29 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_SET_VOLUME:
- AUDIO_TRACK.setVolume((Float) message);
+ float volume = (Float) message;
+ // Workaround: we cannot mute the audio track by setting the volume to 0, we need to
+ // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track
+ // whenever volume is being set might cause side effects, therefore we only handle
+ // "explicit mute operations", i.e., only after certain non-zero volume has been
+ // set, the subsequent volume setting operations will be consider as mute/un-mute
+ // operations and thus enable/disable the audio track.
+ if (mIsMuted && volume > 0) {
+ mIsMuted = false;
+ if (mEnabled) {
+ setStatus(true);
+ }
+ } else if (!mIsMuted && volume == 0) {
+ mIsMuted = true;
+ if (mEnabled) {
+ setStatus(false);
+ }
+ }
+ AUDIO_TRACK.setVolume(volume);
break;
case MSG_SET_AUDIO_TRACK:
- boolean enabled = (Integer) message == 1;
- if (enabled == AUDIO_TRACK.isEnabled()) {
- return;
- }
- if (!enabled) {
- // mAudioClock can be different from getPositionUs. In order to sync them,
- // we set mAudioClock.
- mAudioClock.setPositionUs(getPositionUs());
- }
- AUDIO_TRACK.setStatus(enabled);
- if (enabled) {
- // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
- // the current position. If not, AUDIO_TRACK has the obsolete data.
- seekTo(mAudioClock.getPositionUs());
- }
+ mEnabled = (Integer) message == 1;
+ setStatus(mEnabled);
break;
case MSG_SET_PLAYBACK_SPEED:
mAudioClock.setPlaybackSpeed((Float) message);
@@ -537,4 +582,21 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
super.handleMessage(messageType, message);
}
}
+
+ private void setStatus(boolean enabled) {
+ if (enabled == AUDIO_TRACK.isEnabled()) {
+ return;
+ }
+ if (!enabled) {
+ // mAudioClock can be different from getPositionUs. In order to sync them,
+ // we set mAudioClock.
+ mAudioClock.setPositionUs(getPositionUs());
+ }
+ AUDIO_TRACK.setStatus(enabled);
+ if (enabled) {
+ // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
+ // the current position. If not, AUDIO_TRACK has the obsolete data.
+ seekTo(mAudioClock.getPositionUs());
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java
index 2bf86b5a..604959d1 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java
@@ -25,14 +25,14 @@ import com.google.android.exoplayer.SampleSource;
/**
* MPEG-2 TS audio track renderer.
- * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at
- * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
- * asynchronous Audio/Video outputs.
- * This class calculates the offset of audio data and adjust the presentation times to avoid the
- * asynchronous Audio/Video problem.
+ *
+ * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the
+ * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
+ * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the
+ * presentation times to avoid the asynchronous Audio/Video problem.
*/
-public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
- private final String TAG = "Ac3TrackRenderer";
+public class Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer {
+ private final String TAG = "Ac3MediaCodecTrackRenderer";
private final boolean DEBUG = false;
private final Ac3EventListener mListener;
@@ -47,8 +47,11 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e);
}
- public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
- Handler eventHandler, EventListener eventListener) {
+ public Ac3MediaCodecTrackRenderer(
+ SampleSource source,
+ MediaCodecSelector mediaCodecSelector,
+ Handler eventHandler,
+ EventListener eventListener) {
super(source, mediaCodecSelector, eventHandler, eventListener);
mListener = (Ac3EventListener) eventListener;
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
index bfdf08ac..6f152490 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
@@ -98,8 +98,8 @@ public class AudioTrackMonitor {
long now = SystemClock.elapsedRealtime();
if (mExpireMs != 0 && now >= mExpireMs) {
if (DEBUG) {
- long sampleDuration = (mTotalCount - 1) *
- Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US / 1000;
+ long sampleDuration =
+ (mTotalCount - 1) * Ac3DefaultTrackRenderer.AC3_SAMPLE_DURATION_US / 1000;
long totalDuration = now - mStartMs;
StringBuilder ptsBuilder = new StringBuilder();
ptsBuilder.append("PTS received ").append(mSampleCount).append(", ")
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
index bc3c5d00..393e12c3 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
@@ -18,6 +18,7 @@ package com.android.tv.tuner.exoplayer.ac3;
import android.media.MediaFormat;
+import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioTrack;
import java.nio.ByteBuffer;
@@ -28,6 +29,10 @@ import java.nio.ByteBuffer;
* This wrapper class will do nothing in disabled status for those operations.
*/
public class AudioTrackWrapper {
+ private static final int PCM16_FRAME_BYTES = 2;
+ private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536;
+ private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK =
+ Ac3DefaultTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK;
private final AudioTrack mAudioTrack = new AudioTrack();
private int mAudioSessionID;
private boolean mIsEnabled;
@@ -106,7 +111,7 @@ public class AudioTrackWrapper {
mAudioTrack.setVolume(volume);
}
- public void reconfigure(MediaFormat format) {
+ public void reconfigure(MediaFormat format, int audioBufferSize) {
if (!mIsEnabled || format == null) {
return;
}
@@ -117,9 +122,9 @@ public class AudioTrackWrapper {
try {
pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING);
} catch (Exception e) {
- pcmEncoding = com.google.android.exoplayer.MediaFormat.NO_VALUE;
+ pcmEncoding = C.ENCODING_PCM_16BIT;
}
- // TODO: Handle non-AC3 or non-passthrough audio.
+ // TODO: Handle non-AC3.
if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) {
// Workarounds b/25955476.
// Since all devices and platforms does not support passthrough for non-stereo AC3,
@@ -127,7 +132,14 @@ public class AudioTrackWrapper {
// In other words, the channel count should be always 2.
channelCount = 2;
}
- mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding);
+ if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) {
+ audioBufferSize =
+ channelCount
+ * PCM16_FRAME_BYTES
+ * AC3_FRAMES_IN_ONE_SAMPLE
+ * BUFFERED_SAMPLES_IN_AUDIOTRACK;
+ }
+ mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize);
}
public void handleDiscontinuity() {
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index eb596e93..112e9dc4 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -25,13 +25,14 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.util.Utils;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -59,7 +60,8 @@ public class BufferManager {
private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
// Maps from track name to a map which maps from starting position to {@link SampleChunk}.
- private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>();
+ private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap =
+ new ArrayMap<>();
private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
private final StorageManager mStorageManager;
@@ -77,13 +79,11 @@ public class BufferManager {
}
};
- private volatile boolean mClosed = false;
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;
public interface ChunkEvictedListener {
void onChunkEvicted(String id, long createdTimeMs);
@@ -174,6 +174,66 @@ public class BufferManager {
}
/**
+ * A Track format which will be loaded and saved from the permanent storage for recordings.
+ */
+ public static class TrackFormat {
+
+ /**
+ * The track id for the specified track. The track id will be used as a track identifier
+ * for recordings.
+ */
+ public final String trackId;
+
+ /**
+ * The {@link MediaFormat} for the specified track.
+ */
+ public final MediaFormat format;
+
+ /**
+ * Creates TrackFormat.
+ * @param trackId
+ * @param format
+ */
+ public TrackFormat(String trackId, MediaFormat format) {
+ this.trackId = trackId;
+ this.format = format;
+ }
+ }
+
+ /**
+ * A Holder for a sample position which will be loaded from the index file for recordings.
+ */
+ public static class PositionHolder {
+
+ /**
+ * The current sample position in microseconds.
+ * The position is identical to the PTS(presentation time stamp) of the sample.
+ */
+ public final long positionUs;
+
+ /**
+ * Base sample position for the current {@link SampleChunk}.
+ */
+ public final long basePositionUs;
+
+ /**
+ * The file offset for the current sample in the current {@link SampleChunk}.
+ */
+ public final int offset;
+
+ /**
+ * Creates a holder for a specific position in the recording.
+ * @param positionUs
+ * @param offset
+ */
+ public PositionHolder(long positionUs, long basePositionUs, int offset) {
+ this.positionUs = positionUs;
+ this.basePositionUs = basePositionUs;
+ this.offset = offset;
+ }
+ }
+
+ /**
* Storage configuration and policy manager for {@link BufferManager}
*/
public interface StorageManager {
@@ -186,11 +246,6 @@ public class BufferManager {
File getBufferDir();
/**
- * Cleans up storage.
- */
- void clearStorage();
-
- /**
* Informs whether the storage is used for persistent use. (eg. dvr recording/play)
*
* @return {@code true} if stored files are persistent
@@ -220,29 +275,27 @@ public class BufferManager {
* Reads track name & {@link MediaFormat} from storage.
*
* @param isAudio {@code true} if it is for audio track
- * @return {@link Pair} of track name & {@link MediaFormat}
- * @throws IOException
+ * @return {@link List} of TrackFormat
*/
- Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
+ List<TrackFormat> readTrackInfoFiles(boolean isAudio);
/**
- * Reads sample indexes for each written sample from storage.
+ * Reads key sample positions for each written sample from storage.
*
* @param trackId track name
* @return indexes of the specified track
* @throws IOException
*/
- ArrayList<Long> readIndexFile(String trackId) throws IOException;
+ ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
/**
* Writes track information to storage.
*
- * @param trackId track name
- * @param format {@link android.media.MediaFormat} of the track
+ * @param formatList {@list List} of TrackFormat
* @param isAudio {@code true} if it is for audio track
* @throws IOException
*/
- void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)
throws IOException;
/**
@@ -252,7 +305,7 @@ public class BufferManager {
* @param index {@link SampleChunk} container
* @throws IOException
*/
- void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException;
}
@@ -307,7 +360,6 @@ public class BufferManager {
SampleChunk.SampleChunkCreator sampleChunkCreator) {
mStorageManager = storageManager;
mSampleChunkCreator = sampleChunkCreator;
- clearBuffer(true);
}
public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
@@ -318,44 +370,44 @@ public class BufferManager {
mEvictListeners.remove(id);
}
- private void clearBuffer(boolean deleteFiles) {
- mChunkMap.clear();
- if (deleteFiles) {
- mStorageManager.clearStorage();
- }
- mBufferSize = 0;
- }
-
private static String getFileName(String id, long positionUs) {
return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
}
/**
- * Creates a new {@link SampleChunk} for caching samples.
+ * Creates a new {@link SampleChunk} for caching samples if it is needed.
*
* @param id the name of the track
- * @param positionUs starting position of the {@link SampleChunk} in micro seconds.
+ * @param positionUs current position to write a sample in micro seconds.
* @param samplePool {@link SamplePool} for the fast creation of samples.
+ * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create
+ * a new {@link SampleChunk}.
+ * @param currentOffset the current offset to write.
* @return returns the created {@link SampleChunk}.
* @throws IOException
*/
- public SampleChunk createNewWriteFile(String id, long positionUs,
- SamplePool samplePool) throws IOException {
+ public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool,
+ SampleChunk currentChunk, int currentOffset) throws IOException {
if (!maybeEvictChunk()) {
throw new IOException("Not enough storage space");
}
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(id, map);
mStartPositionMap.put(id, positionUs);
mPendingDelete.init(id);
}
- File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
- SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file,
- positionUs, mChunkCallback);
- map.put(positionUs, sampleChunk);
- return sampleChunk;
+ if (currentChunk == null) {
+ File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
+ SampleChunk sampleChunk = mSampleChunkCreator
+ .createSampleChunk(samplePool, file, positionUs, mChunkCallback);
+ map.put(positionUs, new Pair(sampleChunk, 0));
+ return sampleChunk;
+ } else {
+ map.put(positionUs, new Pair(currentChunk, currentOffset));
+ return null;
+ }
}
/**
@@ -366,10 +418,10 @@ public class BufferManager {
* @throws IOException
*/
public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
- ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
- long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0;
+ ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
+ long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
- SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(trackId, map);
@@ -377,11 +429,15 @@ public class BufferManager {
mPendingDelete.init(trackId);
}
SampleChunk chunk = null;
- for (long positionUs: keyPositions) {
- chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
- mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs,
- mChunkCallback, chunk);
- map.put(positionUs, chunk);
+ long basePositionUs = -1;
+ for (PositionHolder position: keyPositions) {
+ if (position.basePositionUs != basePositionUs) {
+ chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
+ mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs),
+ position.positionUs, mChunkCallback, chunk);
+ basePositionUs = position.basePositionUs;
+ }
+ map.put(position.positionUs, new Pair(chunk, position.offset));
}
}
@@ -392,19 +448,19 @@ public class BufferManager {
* @param positionUs the position.
* @return returns the found {@link SampleChunk}.
*/
- public SampleChunk getReadFile(String id, long positionUs) {
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
return null;
}
- SampleChunk sampleChunk;
- SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1);
+ Pair<SampleChunk, Integer> ret;
+ SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
if (!headMap.isEmpty()) {
- sampleChunk = headMap.get(headMap.lastKey());
+ ret = headMap.get(headMap.lastKey());
} else {
- sampleChunk = map.get(map.firstKey());
+ ret = map.get(map.firstKey());
}
- return sampleChunk;
+ return ret;
}
/**
@@ -439,15 +495,16 @@ public class BufferManager {
// Since chunks are persistent, we cannot evict chunks.
return false;
}
- SortedMap<Long, SampleChunk> earliestChunkMap = null;
+ SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
SampleChunk earliestChunk = null;
String earliestChunkId = null;
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
- SampleChunk chunk = map.get(map.firstKey());
+ SampleChunk chunk = map.get(map.firstKey()).first;
if (earliestChunk == null
|| chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
earliestChunkMap = map;
@@ -473,8 +530,9 @@ public class BufferManager {
}
pendingDelete = mPendingDelete.getSize();
}
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
@@ -489,70 +547,74 @@ public class BufferManager {
* @return returns all track information which is found by {@link BufferManager.StorageManager}.
* @throws IOException
*/
- public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
- ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(false));
- } catch (FileNotFoundException e) {
- // There can be a single track only recording. (eg. audio-only, video-only)
- // So the exception should not stop the read.
+ public List<TrackFormat> readTrackInfoFiles() throws IOException {
+ List<TrackFormat> trackFormatList = new ArrayList<>();
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
+ if (trackFormatList.isEmpty()) {
+ throw new IOException("No track information to load");
}
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(true));
- } catch (FileNotFoundException e) {
- // See above catch block.
- }
- return trackInfos;
+ return trackFormatList;
}
/**
* Writes track information and index information for all tracks.
*
- * @param audio audio information.
- * @param video video information.
+ * @param audios list of audio track information
+ * @param videos list of audio track information
* @throws IOException
*/
- public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)
+ public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
throws IOException {
- if (audio != null) {
- mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first);
- if (map == null) {
- throw new IOException("Audio track index missing");
+ if (audios.isEmpty() && videos.isEmpty()) {
+ throw new IOException("No track information to save");
+ }
+ if (!audios.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(audios, true);
+ for (TrackFormat trackFormat : audios) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Audio track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(audio.first, map);
}
- if (video != null) {
- mStorageManager.writeTrackInfoFile(video.first, video.second, false);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first);
- if (map == null) {
- throw new IOException("Video track index missing");
+ if (!videos.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(videos, false);
+ for (TrackFormat trackFormat : videos) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Video track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(video.first, map);
}
}
/**
- * Marks it is closed and it is not used anymore.
- */
- public void close() {
- // Clean-up may happen after this is called.
- mClosed = true;
- }
-
- /**
* Releases all the resources.
*/
public void release() {
- mPendingDelete.release();
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- for (SampleChunk chunk : entry.getValue().values()) {
- SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
+ try {
+ mPendingDelete.release();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SampleChunk toRelease = null;
+ for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
+ if (toRelease != positions.first) {
+ toRelease = positions.first;
+ SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
+ }
+ }
}
- }
- mChunkMap.clear();
- if (mClosed) {
- clearBuffer(!mStorageManager.isPersistent());
+ mChunkMap.clear();
+ } catch (ConcurrentModificationException | NullPointerException e) {
+ // TODO: remove this after it it confirmed that race condition issues are resolved.
+ // b/32492258, b/32373376
+ SoftPreconditions.checkState(false, "Exception on BufferManager#release: ",
+ e.toString());
}
}
@@ -611,20 +673,6 @@ public class BufferManager {
}
/**
- * Marks {@link BufferManager} object disabled to prevent it from the future use.
- */
- public void disable() {
- mDisabled = true;
- }
-
- /**
- * Returns if {@link BufferManager} object is disabled.
- */
- public boolean isDisabled() {
- return mDisabled;
- }
-
- /**
* Returns if {@link BufferManager} has checked the write speed,
* which is suitable for Trickplay.
*/
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 6a0502a7..bea3defd 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -17,8 +17,12 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.media.MediaFormat;
+import android.util.Log;
import android.util.Pair;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
+import com.google.protobuf.nano.MessageNano;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -28,18 +32,25 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.SortedMap;
/**
* Manages DVR storage.
*/
public class DvrStorageManager implements BufferManager.StorageManager {
+ private static final String TAG = "DvrStorageManager";
// TODO: make serializable classes and use protobuf after internal data structure is finalized.
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
+ private static final String META_FILE_TYPE_AUDIO = "audio";
+ private static final String META_FILE_TYPE_VIDEO = "video";
+ private static final String META_FILE_TYPE_CAPTION = "caption";
private static final String META_FILE_SUFFIX = ".meta";
private static final String IDX_FILE_SUFFIX = ".idx";
+ private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
// Size of minimum reserved storage buffer which will be used to save meta files
// and index files after actual recording finished.
@@ -59,18 +70,6 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void clearStorage() {
- if (mIsRecording) {
- File[] files = mBufferDir.listFiles();
- if (files != null && files.length > 0) {
- for (File file : files) {
- file.delete();
- }
- }
- }
- }
-
- @Override
public File getBufferDir() {
return mBufferDir;
}
@@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
}
+ private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
+ try {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ } catch (IOException e) {
+ // Since we are reading optional field, ignore the exception.
+ }
+ }
+
private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
int len = in.readInt();
if (len <= 0) {
@@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
- String name = readString(in);
- MediaFormat format = new MediaFormat();
- readFormatString(in, format, MediaFormat.KEY_MIME);
- readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- readFormatInt(in, format, MediaFormat.KEY_WIDTH);
- readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
- readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
- readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
- readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- readFormatByteBuffer(in, format, "csd-" + i);
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ String name = readString(in);
+ MediaFormat format = new MediaFormat();
+ readFormatString(in, format, MediaFormat.KEY_MIME);
+ readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ readFormatInt(in, format, MediaFormat.KEY_WIDTH);
+ readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
+ readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
+ readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
+ readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int i = 0; i < 3; ++i) {
+ readFormatByteBuffer(in, format, "csd-" + i);
+ }
+ readFormatLong(in, format, MediaFormat.KEY_DURATION);
+
+ // This is optional since language field is added later.
+ readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
+ trackFormatList.add(new BufferManager.TrackFormat(name, format));
+ } catch (IOException e) {
+ trackNotFound = true;
}
- readFormatLong(in, format, MediaFormat.KEY_DURATION);
- return new Pair<>(name, format);
+ index++;
+ } while(!trackNotFound);
+ return trackFormatList;
+ }
+
+ /**
+ * Reads caption information from files.
+ *
+ * @return a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public List<AtscCaptionTrack> readCaptionInfoFiles() {
+ List<AtscCaptionTrack> tracks = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ byte[] data = new byte[(int) file.length()];
+ in.read(data);
+ tracks.add(AtscCaptionTrack.parseFrom(data));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while(!trackNotFound);
+ return tracks;
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
+ }
+ return indices;
}
}
- @Override
- public ArrayList<Long> readIndexFile(String trackId) throws IOException {
- ArrayList<Long> indices = new ArrayList<>();
- File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
long count = in.readLong();
for (long i = 0; i < count; ++i) {
- indices.add(in.readLong());
+ long positionUs = in.readLong();
+ long basePositionUs = in.readLong();
+ int offset = in.readInt();
+ indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
}
return indices;
}
}
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+ throws IOException {
+ File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
+ if (file.exists()) {
+ return readNewIndexFile(file);
+ } else {
+ return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX));
+ }
+ }
+
private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
throws IOException {
if (format.containsKey(key)) {
@@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
- writeString(out, trackId);
- writeFormatString(out, format, MediaFormat.KEY_MIME);
- writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- writeFormatInt(out, format, MediaFormat.KEY_WIDTH);
- writeFormatInt(out, format, MediaFormat.KEY_HEIGHT);
- writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT);
- writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE);
- writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- writeFormatByteBuffer(out, format, "csd-" + i);
+ for (int i = 0; i < formatList.size() ; ++i) {
+ BufferManager.TrackFormat trackFormat = formatList.get(i);
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ writeString(out, trackFormat.trackId);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
+ writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int j = 0; j < 3; ++j) {
+ writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
+ }
+ writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
+ }
+ }
+ }
+
+ /**
+ * Writes caption information to files.
+ *
+ * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
+ if (tracks == null || tracks.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < tracks.size(); i++) {
+ AtscCaptionTrack track = tracks.get(i);
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ out.write(MessageNano.toByteArray(track));
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to write caption info to files", e);
}
- writeFormatLong(out, format, MediaFormat.KEY_DURATION);
}
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException {
- File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX);
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
out.writeLong(index.size());
- for (Long key : index.keySet()) {
- out.writeLong(key);
+ for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
+ out.writeLong(entry.getKey());
+ out.writeLong(entry.getValue().first.getStartPositionUs());
+ out.writeInt(entry.getValue().second);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index 4869b49f..af0c3f0d 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public static final int BUFFER_REASON_RECORDING = 2;
/**
- * The duration of a chunk of samples, {@link SampleChunk}.
+ * The minimum duration to support seek in Trickplay.
*/
- static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+ static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+
+ /**
+ * The duration of a {@link SampleChunk} for recordings.
+ */
+ static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes
private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds
private static final long BUFFER_NEEDED_US =
1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
@@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private int mTrackCount;
private boolean[] mTrackSelected;
- private List<String> mIds;
private List<SampleQueue> mReadSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
@@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackCount <= 0) {
throw new IOException("No tracks to initialize");
}
- mIds = ids;
mTrackSelected = new boolean[mTrackCount];
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
@@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
mSampleChunkIoHelper.init();
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
+ }
}
@Override
@@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (!mTrackSelected[index]) {
mTrackSelected[index] = true;
mReadSampleQueues.get(index).clear();
- mBufferManager.registerChunkEvictedListener(mIds.get(index),
- RecordingSampleBuffer.this);
mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
}
}
@@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackSelected[index]) {
mTrackSelected[index] = false;
mReadSampleQueues.get(index).clear();
- mBufferManager.unregisterChunkEvictedListener(mIds.get(index));
+ mSampleChunkIoHelper.closeRead(index);
}
}
@@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
Log.w(TAG, "Disk is too slow for trickplay");
- mBufferManager.disable();
mBufferListener.onDiskTooSlow();
}
@@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private boolean maybeReadSample(SampleQueue queue, int index) {
if (queue.getLastQueuedPositionUs() != null
&& queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
- && queue.isDurationGreaterThan(CHUNK_DURATION_US)) {
+ && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) {
// The speed of queuing samples can be higher than the playback speed.
// If the duration of the samples in the queue is not limited,
// samples can be accumulated and there can be out-of-memory issues.
@@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public void onChunkEvicted(String id, long createdTimeMs) {
if (mBufferListener != null) {
mBufferListener.onBufferStartTimeChanged(
- createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
+ createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
index 552caaef..ab6d1a75 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -151,18 +151,23 @@ public class SampleChunk {
mCurrentOffset = 0;
}
+ private void reset(SampleChunk chunk, long offset) {
+ mChunk = chunk;
+ mCurrentOffset = offset;
+ }
+
/**
* Prepares for read I/O operation from a new SampleChunk.
*
* @param chunk the new SampleChunk to read from
* @throws IOException
*/
- void openRead(SampleChunk chunk) throws IOException {
+ void openRead(SampleChunk chunk, long offset) throws IOException {
if (mChunk != null) {
mChunk.closeRead();
}
chunk.openRead();
- reset(chunk);
+ reset(chunk, offset);
}
/**
@@ -241,6 +246,20 @@ public class SampleChunk {
}
/**
+ * Returns the current SampleChunk for subsequent I/O operation.
+ */
+ SampleChunk getChunk() {
+ return mChunk;
+ }
+
+ /**
+ * Returns the current offset of the current SampleChunk for subsequent I/O operation.
+ */
+ long getOffset() {
+ return mCurrentOffset;
+ }
+
+ /**
* Releases SampleChunk. the SampleChunk will not be used anymore.
*
* @param chunk to release
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index 37ae4022..ca97a91a 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -21,6 +21,7 @@ import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import java.io.IOException;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
@@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback {
private static final int MSG_OPEN_READ = 1;
private static final int MSG_OPEN_WRITE = 2;
- private static final int MSG_CLOSE_WRITE = 3;
- private static final int MSG_READ = 4;
- private static final int MSG_WRITE = 5;
- private static final int MSG_RELEASE = 6;
+ private static final int MSG_CLOSE_READ = 3;
+ private static final int MSG_CLOSE_WRITE = 4;
+ private static final int MSG_READ = 5;
+ private static final int MSG_WRITE = 6;
+ private static final int MSG_RELEASE = 7;
+ private final long mSampleChunkDurationUs;
private final int mTrackCount;
private final List<String> mIds;
private final List<MediaFormat> mMediaFormats;
@@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback {
private Handler mIoHandler;
private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[];
- private final long[] mWriteEndPositionUs;
+ private final long[] mWriteIndexEndPositionUs;
+ private final long[] mWriteChunkEndPositionUs;
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
+ private final Set<Integer> mSelectedTracks = new ArraySet<>();
private long mBufferDurationUs = 0;
private boolean mWriteEnded;
private boolean mErrorNotified;
@@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback {
mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
- mWriteEndPositionUs = new long[mTrackCount];
+ mWriteIndexEndPositionUs = new long[mTrackCount];
+ mWriteChunkEndPositionUs = new long[mTrackCount];
mReadIoStates = new SampleChunk.IoState[mTrackCount];
mWriteIoStates = new SampleChunk.IoState[mTrackCount];
+
+ // Small chunk duration for live playback will give more fine grained storage usage
+ // and eviction handling for trickplay.
+ mSampleChunkDurationUs =
+ bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ?
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US :
+ RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US;
for (int i = 0; i < mTrackCount; ++i) {
- mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US;
+ mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs;
mReadIoStates[i] = new SampleChunk.IoState();
mWriteIoStates[i] = new SampleChunk.IoState();
}
@@ -204,6 +220,15 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
/**
+ * Closes read from the specified track.
+ *
+ * @param index track index
+ */
+ public void closeRead(int index) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index));
+ }
+
+ /**
* Notifies writes are finished.
*/
public void closeWrite() {
@@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback {
try {
if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
// Saves meta information for recording.
- Pair<String, android.media.MediaFormat> audio = null, video = null;
+ List<BufferManager.TrackFormat> audios = new LinkedList<>();
+ List<BufferManager.TrackFormat> videos = new LinkedList<>();
for (int i = 0; i < mTrackCount; ++i) {
android.media.MediaFormat format =
mMediaFormats.get(i).getFrameworkMediaFormatV16();
format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
- if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
- audio = new Pair<>(mIds.get(i), format);
- } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
- video = new Pair<>(mIds.get(i), format);
- }
- if (audio != null && video != null) {
- break;
+ if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+ audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+ videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
}
}
- mBufferManager.writeMetaFiles(audio, video);
+ mBufferManager.writeMetaFiles(audios, videos);
}
} finally {
mBufferManager.release();
@@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback {
case MSG_OPEN_WRITE:
doOpenWrite((int) message.obj);
return true;
+ case MSG_CLOSE_READ:
+ doCloseRead((int) message.obj);
+ return true;
case MSG_CLOSE_WRITE:
doCloseWrite();
return true;
@@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback {
private void doOpenRead(IoParams params) throws IOException {
int index = params.index;
mIoHandler.removeMessages(MSG_READ, index);
- SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs);
- if (chunk == null) {
+ Pair<SampleChunk, Integer> readPosition =
+ mBufferManager.getReadFile(mIds.get(index), params.positionUs);
+ if (readPosition == null) {
String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs
+ "is not found";
- SoftPreconditions.checkNotNull(chunk, TAG, errorMessage);
+ SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
throw new IOException(errorMessage);
}
- mReadIoStates[index].openRead(chunk);
+ mSelectedTracks.add(index);
+ mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second);
if (mHandlerReadSampleBuffers[index] != null) {
SampleHolder sample;
while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
@@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doOpenWrite(int index) throws IOException {
- SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool);
+ SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0,
+ mSamplePool, null, 0);
mWriteIoStates[index].openWrite(chunk);
}
+ private void doCloseRead(int index) {
+ mSelectedTracks.remove(index);
+ if (mHandlerReadSampleBuffers[index] != null) {
+ SampleHolder sample;
+ while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
+ mSamplePool.releaseSample(sample);
+ }
+ }
+ mIoHandler.removeMessages(MSG_READ, index);
+ }
+
private void doRead(int index) throws IOException {
mIoHandler.removeMessages(MSG_READ, index);
if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) {
@@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback {
if (sample.timeUs > mBufferDurationUs) {
mBufferDurationUs = sample.timeUs;
}
-
- if (sample.timeUs >= mWriteEndPositionUs[index]) {
- nextChunk = mBufferManager.createNewWriteFile(mIds.get(index),
- mWriteEndPositionUs[index], mSamplePool);
- mWriteEndPositionUs[index] =
- ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) *
- RecordingSampleBuffer.CHUNK_DURATION_US;
+ if (sample.timeUs >= mWriteIndexEndPositionUs[index]) {
+ SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ?
+ null : mWriteIoStates[params.index].getChunk();
+ int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+ nextChunk = mBufferManager.createNewWriteFileIfNeeded(
+ mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool,
+ currentChunk, currentOffset);
+ mWriteIndexEndPositionUs[index] =
+ ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) *
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ if (nextChunk != null) {
+ mWriteChunkEndPositionUs[index] =
+ ((sample.timeUs / mSampleChunkDurationUs) + 1)
+ * mSampleChunkDurationUs;
+ }
}
}
mWriteIoStates[params.index].write(params.sample, nextChunk);
@@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.removeCallbacksAndMessages(null);
mFinished = true;
conditionVariable.open();
+ mSelectedTracks.clear();
}
private void releaseEvictedChunks() {
- if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) {
+ if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK
+ || mSelectedTracks.isEmpty()) {
return;
}
+ long currentStartPositionUs = Long.MAX_VALUE;
+ for (int trackIndex : mSelectedTracks) {
+ currentStartPositionUs = Math.min(currentStartPositionUs,
+ mReadIoStates[trackIndex].getStartPositionUs());
+ }
for (int i = 0; i < mTrackCount; ++i) {
long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)),
- mReadIoStates[i].getStartPositionUs());
+ currentStartPositionUs);
mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs);
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
index 7b098f40..75eac5a2 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
@@ -43,6 +43,7 @@ public class SampleQueue {
if (sampleFromQueue == null) {
return SampleSource.NOTHING_READ;
}
+ sample.ensureSpaceForWrite(sampleFromQueue.size);
sample.size = sampleFromQueue.size;
sample.flags = sampleFromQueue.flags;
sample.timeUs = sampleFromQueue.timeUs;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 40c4ef95..0b219b41 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -19,6 +19,7 @@ package com.android.tv.tuner.exoplayer.buffer;
import android.os.ConditionVariable;
import android.support.annotation.NonNull;
+
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index 258a5cd0..9fe921b8 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -17,20 +17,23 @@
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.support.annotation.NonNull;
import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+
import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import java.util.SortedMap;
/**
* Manages Trickplay storage.
*/
public class TrickplayStorageManager implements BufferManager.StorageManager {
+ // TODO: Support multi-sessions.
private static final String BUFFER_DIR = "timeshift";
// Copied from android.provider.Settings.Global (hidden fields)
@@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024;
- private final File mBufferDir;
+ private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask;
+ private static File sBufferDir;
+ private static long sStorageBufferBytes;
+
private final long mMaxBufferSize;
- private final long mStorageBufferBytes;
- private static long getStorageBufferBytes(Context context, File path) {
+ private static void initParamsIfNeeded(Context context, @NonNull File path) {
+ // TODO: Support multi-sessions.
+ SoftPreconditions.checkState(
+ sBufferDir == null || sBufferDir.equals(path));
+ if (path.equals(sBufferDir)) {
+ return;
+ }
+ sBufferDir = path;
long lowPercentage = Settings.Global.getInt(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
- long lowBytes = path.getTotalSpace() * lowPercentage / 100;
+ long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100;
long maxLowBytes = Settings.Global.getLong(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
- return Math.min(lowBytes, maxLowBytes);
+ sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes);
}
- public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) {
- mBufferDir = new File(baseDir, BUFFER_DIR);
- mBufferDir.mkdirs();
+ public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) {
+ initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR));
+ sBufferDir.mkdirs();
mMaxBufferSize = maxBufferSize;
clearStorage();
- mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir);
}
- @Override
- public void clearStorage() {
- File files[] = mBufferDir.listFiles();
- if (files == null || files.length == 0) {
- return;
+ private void clearStorage() {
+ long now = System.currentTimeMillis();
+ if (sLastCacheCleanUpTask != null) {
+ sLastCacheCleanUpTask.cancel(true);
}
- if (Looper.myLooper() == Looper.getMainLooper()) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- for (File file : files) {
+ sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+ File files[] = sBufferDir.listFiles();
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for (File file : files) {
+ if (isCancelled()) {
+ break;
+ }
+ long lastModified = file.lastModified();
+ if (lastModified != 0 && lastModified < now) {
file.delete();
}
- return null;
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- for (File file : files) {
- file.delete();
+ return null;
}
- }
+ };
+ sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public File getBufferDir() {
- return mBufferDir;
+ return sBufferDir;
}
@Override
@@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public boolean hasEnoughBuffer(long pendingDelete) {
- return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes;
+ return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes;
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) {
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
return null;
}
@Override
- public ArrayList<Long> readIndexFile(String trackId) {
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) {
return null;
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) {
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) {
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) {
+ public void writeIndexFile(String trackName,
+ SortedMap<Long, Pair<SampleChunk, Integer>> index) {
}
}
diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
index 97d9ece3..53678a85 100644
--- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
+++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
@@ -36,6 +36,24 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment {
"com.android.tv.tuner.setup.ConnectionTypeFragment";
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ ((TunerSetupActivity) getActivity()).clearTunerHal();
+ super.onDestroy();
+ }
+
+ @Override
protected SetupGuidedStepFragment onCreateContentFragment() {
return new ContentFragment();
}
diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
new file mode 100644
index 00000000..a4dd494c
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.setup;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.tuner.util.PostalCodeUtils;
+
+import java.util.List;
+
+/**
+ * A fragment for initial screen.
+ */
+public class PostalCodeFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY =
+ "com.android.tv.tuner.setup.PostalCodeFragment";
+ private static final int VIEW_TYPE_EDITABLE = 1;
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ ContentFragment fragment = new ContentFragment();
+ Bundle arguments = new Bundle();
+ arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return true;
+ }
+
+ @Override
+ protected boolean needsSkipButton() {
+ return true;
+ }
+
+ @Override
+ protected void setOnClickAction(View view, final String category, final int actionId) {
+ if (actionId == ACTION_DONE) {
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ CharSequence postalCode =
+ ((ContentFragment) getContentFragment()).mEditAction.getTitle();
+ if (postalCode != null && postalCode.length() == 5) {
+ PostalCodeUtils.setLastPostalCode(getContext(), postalCode.toString());
+ onActionClick(category, actionId);
+ } else {
+ ContentFragment contentFragment = (ContentFragment) getContentFragment();
+ contentFragment.mEditAction.setDescription(
+ getString(R.string.postal_code_invalid_warning));
+ contentFragment.notifyActionChanged(0);
+ contentFragment.mEditedActionView.performClick();
+ }
+ }
+ });
+ } else if (actionId == ACTION_SKIP) {
+ super.setOnClickAction(view, category, ACTION_SKIP);
+ }
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
+ private GuidedAction mEditAction;
+ private View mEditedActionView;
+ private View mDoneActionView;
+ private boolean mProceed;
+
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ if (action.equals(mEditAction)) {
+ if (mProceed) {
+ // "NEXT" in IME was just clicked, moves focus to Done button.
+ if (mDoneActionView == null) {
+ mDoneActionView = getActivity().findViewById(R.id.button_done);
+ }
+ mDoneActionView.requestFocus();
+ mProceed = false;
+ } else {
+ // Directly opens IME to input postal/zip code.
+ if (mEditedActionView == null) {
+ mEditedActionView = getView().findViewById(R.id.guidedactions_editable);
+ ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title))
+ .setFilters(new InputFilter[]{new InputFilter() {
+ @Override
+ public CharSequence filter(CharSequence source, int start,
+ int end, Spanned dest, int dstart, int dend) {
+ try {
+ Integer.parseInt(source.toString());
+ return null;
+ } catch (NumberFormatException e) {
+ return "";
+ }
+ }
+ }, new InputFilter.LengthFilter(5)});
+ }
+ mEditedActionView.performClick();
+ }
+ }
+ }
+
+ @Override
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ mProceed = true;
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.postal_code_guidance_title);
+ String description = getString(R.string.postal_code_guidance_description);
+ String breadcrumb = getString(R.string.ut_setup_breadcrumb);
+ return new Guidance(title, description, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ String description = getString(R.string.postal_code_action_description);
+ mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true)
+ .description(description).build();
+ actions.add(mEditAction);
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist() {
+ @Override
+ public int getItemViewType(GuidedAction action) {
+ if (action.isEditable()) {
+ return VIEW_TYPE_EDITABLE;
+ }
+ return super.getItemViewType(action);
+ }
+
+ @Override
+ public int onProvideItemLayoutId(int viewType) {
+ if (viewType == VIEW_TYPE_EDITABLE) {
+ return R.layout.guided_action_editable;
+ }
+ return super.onProvideItemLayoutId(viewType);
+ }
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java
index 3b61debb..75b28e32 100644
--- a/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -21,6 +21,7 @@ import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -35,14 +36,13 @@ import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
-import com.android.tv.common.AutoCloseableUtils;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.tuner.ChannelScanFileParser;
-import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
-import com.android.tv.tuner.data.nano.Channel;
+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;
@@ -51,7 +51,6 @@ import com.android.tv.tuner.source.TsStreamer;
import com.android.tv.tuner.source.TunerTsStreamer;
import com.android.tv.tuner.tvinput.ChannelDataManager;
import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
import junit.framework.Assert;
@@ -67,6 +66,7 @@ import java.util.concurrent.TimeUnit;
public class ScanFragment extends SetupFragment {
private static final String TAG = "ScanFragment";
private static final boolean DEBUG = false;
+
// In the fake mode, the connection to antenna or cable is not necessary.
// Instead dummy channels are added.
private static final boolean FAKE_MODE = false;
@@ -98,6 +98,7 @@ public class ScanFragment extends SetupFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreateView");
View view = super.onCreateView(inflater, container, savedInstanceState);
mChannelDataManager = new ChannelDataManager(getActivity());
mChannelDataManager.checkDataVersion(getActivity());
@@ -120,13 +121,19 @@ public class ScanFragment extends SetupFragment {
}
});
Bundle args = getArguments();
+ int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
// 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())){
- scanTitleView.setText(R.string.bt_channel_scan);
- } else {
- scanTitleView.setText(R.string.ut_channel_scan);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ scanTitleView.setText(R.string.ut_channel_scan);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ scanTitleView.setText(R.string.nt_channel_scan);
+ break;
+ default:
+ scanTitleView.setText(R.string.bt_channel_scan);
}
return view;
}
@@ -147,12 +154,14 @@ public class ScanFragment extends SetupFragment {
}
@Override
- public void onDetach() {
+ public void onPause() {
+ Log.d(TAG, "onPause");
if (mChannelScanTask != null) {
// Ensure scan task will stop.
+ Log.w(TAG, "The activity went to the background. Stopping channel scan.");
mChannelScanTask.stopScan();
}
- super.onDetach();
+ super.onPause();
}
/**
@@ -168,7 +177,9 @@ public class ScanFragment extends SetupFragment {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
- mChannelScanTask.showFinishingProgressDialog();
+ if (mChannelScanTask != null) {
+ mChannelScanTask.showFinishingProgressDialog();
+ }
}
}, SHOW_PROGRESS_DIALOG_DELAY_MS);
@@ -255,7 +266,7 @@ public class ScanFragment extends SetupFragment {
if (FAKE_MODE) {
mScanTsStreamer = new FakeTsStreamer(this);
} else {
- TunerHal hal = TunerHal.createInstance(mActivity.getApplicationContext());
+ TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal();
if (hal == null) {
throw new RuntimeException("Failed to open a DVB device");
}
@@ -316,10 +327,17 @@ public class ScanFragment extends SetupFragment {
@Override
protected void onProgressUpdate(Integer... values) {
- mProgressBar.setProgress(values[0]);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mProgressBar.setProgress(values[0], true);
+ } else {
+ mProgressBar.setProgress(values[0]);
+ }
}
private void stopScan() {
+ if (mLatch != null) {
+ mLatch.countDown();
+ }
mConditionStopped.open();
}
@@ -360,11 +378,7 @@ public class ScanFragment extends SetupFragment {
if (mConditionStopped.block(-1)) {
break;
}
- onProgressUpdate(MAX_PROGRESS * i++ / mScanChannelList.size());
- }
- if (mScanTsStreamer instanceof TunerTsStreamer) {
- AutoCloseableUtils.closeQuietly(
- ((TunerTsStreamer) mScanTsStreamer).getTunerHal());
+ publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size());
}
mChannelDataManager.notifyScanCompleted();
if (!mConditionStopped.block(-1)) {
@@ -454,7 +468,13 @@ public class ScanFragment extends SetupFragment {
if (mFinishingProgressDialog != null) {
mFinishingProgressDialog.dismiss();
}
- onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ // If the fragment is not resumed, the next fragment (scan result page) can't be
+ // displayed. In that case, just close the activity.
+ if (isResumed()) {
+ onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ } else if (getActivity() != null) {
+ getActivity().finish();
+ }
mChannelScanTask = null;
}
}
diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java
index 068543cd..3b8cd823 100644
--- a/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -26,6 +26,7 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
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.util.TunerInputInfoUtils;
@@ -76,11 +77,19 @@ public class ScanResultFragment extends SetupMultiPaneFragment {
mChannelCountOnPreference, mChannelCountOnPreference);
breadcrumb = null;
} else {
+ Bundle args = getArguments();
+ int tunerType =
+ (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
title = getString(R.string.ut_result_not_found_title);
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- description = getString(R.string.bt_result_not_found_description);
- } else {
- description = getString(R.string.ut_result_not_found_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ description = getString(R.string.ut_result_not_found_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ description = getString(R.string.nt_result_not_found_description);
+ break;
+ default:
+ description = getString(R.string.bt_result_not_found_description);
}
breadcrumb = getString(R.string.ut_setup_breadcrumb);
}
diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
index 78121bc5..f618c699 100644
--- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java
+++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
@@ -29,35 +29,53 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.tv.TvContract;
+import android.os.AsyncTask;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;
import com.android.tv.TvApplication;
+import com.android.tv.common.AutoCloseableUtils;
import com.android.tv.common.TvCommonConstants;
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.experiments.Experiments;
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;
+import com.android.tv.tuner.util.PostalCodeUtils;
+import com.android.tv.util.LocationUtils;
+
+import java.util.Locale;
+import java.util.concurrent.Executor;
/**
* An activity that serves tuner setup process.
*/
public class TunerSetupActivity extends SetupActivity {
- private final String TAG = "TunerSetupActivity";
+ private static final String TAG = "TunerSetupActivity";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Key for passing tuner type to sub-fragments.
+ */
+ public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType";
+
// For the recommendation card
private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity";
private static final String NOTIFY_TAG = "TunerSetup";
private static final int NOTIFY_ID = 1000;
private static final String TAG_DRAWABLE = "drawable";
private static final String TAG_ICON = "ic_launcher_s";
+ private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1;
private static final int CHANNEL_MAP_SCAN_FILE[] = {
R.raw.ut_us_atsc_center_frequencies_8vsb,
@@ -69,9 +87,13 @@ public class TunerSetupActivity extends SetupActivity {
R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256};
private ScanFragment mLastScanFragment;
+ private Integer mTunerType;
+ private TunerHalFactory mTunerHalFactory;
+ private boolean mNeedToShowPostalCodeFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
TvApplication.setCurrentRunningProcess(this, false);
super.onCreate(savedInstanceState);
// TODO: check {@link shouldShowRequestPermissionRationale}.
@@ -79,13 +101,49 @@ public class TunerSetupActivity extends SetupActivity {
!= PackageManager.PERMISSION_GRANTED) {
// No need to check the request result.
requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
- 0);
+ PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+ }
+ mTunerType = TunerHal.getTunerTypeAndCount(this).first;
+ if (mTunerType == null) {
+ finish();
+ } else {
+ mTunerHalFactory = new TunerHalFactory(getApplicationContext());
+ }
+ try {
+ // Updating postal code takes time, therefore we called it here for "warm-up".
+ PostalCodeUtils.setLastPostalCode(this, null);
+ PostalCodeUtils.updatePostalCode(this);
+ } catch (Exception e) {
+ // Do nothing. If the last known postal code is null, we'll show guided fragment to
+ // prompt users to input postal code before ConnectionTypeFragment is shown.
+ Log.i(TAG, "Can't get postal code:" + e);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ && Experiments.CLOUD_EPG.get()) {
+ try {
+ // Updating postal code takes time, therefore we should update postal code
+ // right after the permission is granted, so that the subsequent operations,
+ // especially EPG fetcher, could get the newly updated postal code.
+ PostalCodeUtils.updatePostalCode(this);
+ } catch (Exception e) {
+ // Do nothing
+ }
+ }
}
}
@Override
protected Fragment onCreateInitialFragment() {
SetupFragment fragment = new WelcomeFragment();
+ Bundle args = new Bundle();
+ args.putInt(KEY_TUNER_TYPE, mTunerType);
+ fragment.setArguments(args);
fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION
| SetupFragment.FRAGMENT_REENTER_TRANSITION);
return fragment;
@@ -102,33 +160,41 @@ public class TunerSetupActivity extends SetupActivity {
finish();
break;
default: {
- SetupFragment fragment = new ConnectionTypeFragment();
- fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
- | SetupFragment.FRAGMENT_RETURN_TRANSITION);
- showFragment(fragment, true);
+ if (mNeedToShowPostalCodeFragment
+ || Locale.US.getCountry().equalsIgnoreCase(
+ LocationUtils.getCurrentCountry(getApplicationContext()))
+ && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this))) {
+ // We cannot get postal code automatically. Postal code input fragment
+ // should always be shown even if users have input some valid postal
+ // code in this activity before.
+ mNeedToShowPostalCodeFragment = true;
+ showPostalCodeFragment();
+ } else {
+ showConnectionTypeFragment();
+ }
break;
}
}
return true;
+ case PostalCodeFragment.ACTION_CATEGORY:
+ if (actionId == SetupMultiPaneFragment.ACTION_DONE
+ || actionId == SetupMultiPaneFragment.ACTION_SKIP) {
+ showConnectionTypeFragment();
+ }
+ return true;
case ConnectionTypeFragment.ACTION_CATEGORY:
- TunerHal hal = TunerHal.createInstance(getApplicationContext());
- if (hal == null) {
+ if (mTunerHalFactory.get() == 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,
+ Bundle args1 = new Bundle();
+ args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE,
CHANNEL_MAP_SCAN_FILE[actionId]);
- mLastScanFragment.setArguments(args);
+ args1.putInt(KEY_TUNER_TYPE, mTunerType);
+ mLastScanFragment.setArguments(args1);
showFragment(mLastScanFragment, true);
return true;
case ScanFragment.ACTION_CATEGORY:
@@ -137,7 +203,11 @@ public class TunerSetupActivity extends SetupActivity {
getFragmentManager().popBackStack();
return true;
case ScanFragment.ACTION_FINISH:
+ mTunerHalFactory.clear();
SetupFragment fragment = new ScanResultFragment();
+ Bundle args2 = new Bundle();
+ args2.putInt(KEY_TUNER_TYPE, mTunerType);
+ fragment.setArguments(args2);
fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION
| SetupFragment.FRAGMENT_REENTER_TRANSITION);
showFragment(fragment, true);
@@ -213,7 +283,7 @@ public class TunerSetupActivity extends SetupActivity {
String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(),
TunerTvInputService.class.getName()));
- // Make an intent to launch the setup activity of USB tuner TV input.
+ // Make an intent to launch the setup activity of TV tuner input.
Intent intent = TvCommonUtils.createSetupIntent(
new Intent(context, TunerSetupActivity.class), inputId);
intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId);
@@ -224,6 +294,27 @@ public class TunerSetupActivity extends SetupActivity {
}
/**
+ * Gets the currently used tuner HAL.
+ */
+ TunerHal getTunerHal() {
+ return mTunerHalFactory.get();
+ }
+
+ /**
+ * Generates tuner HAL.
+ */
+ void generateTunerHal() {
+ mTunerHalFactory.generate();
+ }
+
+ /**
+ * Clears the currently used tuner HAL.
+ */
+ void clearTunerHal() {
+ mTunerHalFactory.clear();
+ }
+
+ /**
* Returns a {@link PendingIntent} to launch the tuner TV input service.
*
* @param context a {@link Context} instance
@@ -242,12 +333,19 @@ public class TunerSetupActivity extends SetupActivity {
Resources resources = context.getResources();
String focusedTitle = resources.getString(
R.string.ut_setup_recommendation_card_focused_title);
- String title;
- if (TunerInputInfoUtils.isBuiltInTuner(context)) {
- title = resources.getString(R.string.bt_setup_recommendation_card_title);
- } else {
- title = resources.getString(R.string.ut_setup_recommendation_card_title);
+ int titleStringId = 0;
+ switch (TunerHal.getTunerTypeAndCount(context).first) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ titleStringId = R.string.bt_setup_recommendation_card_title;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ titleStringId = R.string.ut_setup_recommendation_card_title;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ titleStringId = R.string.nt_setup_recommendation_card_title;
+ break;
}
+ String title = resources.getString(titleStringId);
Bitmap largeIcon = BitmapFactory.decodeResource(resources,
R.drawable.recommendation_antenna);
@@ -269,6 +367,20 @@ public class TunerSetupActivity extends SetupActivity {
notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification);
}
+ private void showPostalCodeFragment() {
+ SetupFragment fragment = new PostalCodeFragment();
+ fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
+ }
+
+ private void showConnectionTypeFragment() {
+ SetupFragment fragment = new ConnectionTypeFragment();
+ fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
+ }
+
/**
* Cancels the previously shown recommendation card.
*
@@ -279,4 +391,80 @@ public class TunerSetupActivity extends SetupActivity {
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID);
}
-}
+
+ @VisibleForTesting
+ static class TunerHalFactory {
+ private Context mContext;
+ @VisibleForTesting
+ TunerHal mTunerHal;
+ private GenerateTunerHalTask mGenerateTunerHalTask;
+ private final Executor mExecutor;
+
+ TunerHalFactory(Context context) {
+ this(context, AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ TunerHalFactory(Context context, Executor executor) {
+ mContext = context;
+ mExecutor = executor;
+ }
+
+ /**
+ * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated
+ * before, tries to generate it synchronously.
+ */
+ TunerHal get() {
+ if (mGenerateTunerHalTask != null
+ && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) {
+ try {
+ return mGenerateTunerHalTask.get();
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot get Tuner HAL: " + e);
+ }
+ } else if (mGenerateTunerHalTask == null && mTunerHal == null) {
+ mTunerHal = createInstance();
+ }
+ return mTunerHal;
+ }
+
+ /**
+ * Generates tuner hal for scanning with asynchronous tasks.
+ */
+ void generate() {
+ if (mGenerateTunerHalTask == null && mTunerHal == null) {
+ mGenerateTunerHalTask = new GenerateTunerHalTask();
+ mGenerateTunerHalTask.executeOnExecutor(mExecutor);
+ }
+ }
+
+ /**
+ * Clears the currently used tuner hal.
+ */
+ void clear() {
+ if (mGenerateTunerHalTask != null) {
+ mGenerateTunerHalTask.cancel(true);
+ mGenerateTunerHalTask = null;
+ }
+ if (mTunerHal != null) {
+ AutoCloseableUtils.closeQuietly(mTunerHal);
+ mTunerHal = null;
+ }
+ }
+
+ protected TunerHal createInstance() {
+ return TunerHal.createInstance(mContext);
+ }
+
+ class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
+ @Override
+ protected TunerHal doInBackground(Void... args) {
+ return createInstance();
+ }
+
+ @Override
+ protected void onPostExecute(TunerHal tunerHal) {
+ mTunerHal = tunerHal;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 7e809411..a3dddc72 100644
--- a/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -27,6 +27,7 @@ import android.view.ViewGroup;
import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
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.util.TunerInputInfoUtils;
@@ -41,7 +42,9 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
@Override
protected SetupGuidedStepFragment onCreateContentFragment() {
- return new ContentFragment();
+ ContentFragment fragment = new ContentFragment();
+ fragment.setArguments(getArguments());
+ return fragment;
}
@Override
@@ -70,20 +73,33 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title;
String description;
+ int tunerType = getArguments().getInt(TunerSetupActivity.KEY_TUNER_TYPE,
+ TunerHal.TUNER_TYPE_BUILT_IN);
if (mChannelCountOnPreference == 0) {
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- title = getString(R.string.bt_setup_new_title);
- description = getString(R.string.bt_setup_new_description);
- } else {
- title = getString(R.string.ut_setup_new_title);
- description = getString(R.string.ut_setup_new_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ title = getString(R.string.ut_setup_new_title);
+ description = getString(R.string.ut_setup_new_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ title = getString(R.string.nt_setup_new_title);
+ description = getString(R.string.nt_setup_new_description);
+ break;
+ default:
+ title = getString(R.string.bt_setup_new_title);
+ description = getString(R.string.bt_setup_new_description);
}
} else {
title = getString(R.string.bt_setup_again_title);
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- description = getString(R.string.bt_setup_again_description);
- } else {
- description = getString(R.string.ut_setup_again_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ description = getString(R.string.ut_setup_again_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ description = getString(R.string.nt_setup_again_description);
+ break;
+ default:
+ description = getString(R.string.bt_setup_again_description);
}
}
return new Guidance(title, description, null, null);
diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java
index 14997ee4..80ec8a56 100644
--- a/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -256,7 +256,7 @@ public class FileTsStreamer implements TsStreamer {
* Returns whether the current pid filter is empty or not.
*/
public boolean isFilterEmpty() {
- return mPids.size() > 0;
+ return mPids.size() == 0;
}
/**
diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java
index ccbb75ba..32504b95 100644
--- a/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -17,8 +17,10 @@
package com.android.tv.tuner.source;
import android.content.Context;
+import android.support.annotation.VisibleForTesting;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -127,6 +129,14 @@ public class TsDataSourceManager {
}
/**
+ * Add tuner hal into TunerTsStreamerManager for test.
+ */
+ @VisibleForTesting
+ public void addTunerHalForTest(TunerHal tunerHal) {
+ mTunerStreamerManager.addTunerHal(tunerHal, mId);
+ }
+
+ /**
* Releases persistent resources.
*/
public void release() {
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java
index b24048e6..65b11a5a 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -42,15 +42,17 @@ public class TunerTsStreamer implements TsStreamer {
private static final int MIN_READ_UNIT = 1500;
private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB
private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB
+ private static final int TS_PACKET_SIZE = 188;
private static final int READ_TIMEOUT_MS = 5000; // 5 secs.
private static final int BUFFER_UNDERRUN_SLEEP_MS = 10;
+ private static final int READ_ERROR_STREAMING_ENDED = -1;
+ private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2;
private final Object mCircularBufferMonitor = new Object();
private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE];
private long mBytesFetched;
private final AtomicLong mLastReadPosition = new AtomicLong();
- private boolean mEndOfStreamSent;
private boolean mStreaming;
private final TunerHal mTunerHal;
@@ -59,6 +61,7 @@ public class TunerTsStreamer implements TsStreamer {
private final EventDetector mEventDetector;
private final TsStreamWriter mTsStreamWriter;
+ private String mChannelNumber;
public static class TunerDataSource extends TsDataSource {
private final TunerTsStreamer mTsStreamer;
@@ -103,6 +106,15 @@ public class TunerTsStreamer implements TsStreamer {
offset, readLength);
if (ret > 0) {
mLastReadPosition.addAndGet(ret);
+ } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) {
+ long currentPosition = mStartBufferedPosition + mLastReadPosition.get();
+ long endPosition = mTsStreamer.getBufferedPosition();
+ long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE)
+ * TS_PACKET_SIZE;
+ Log.w(TAG, "Demux position jump by overwritten buffer: " + diff);
+ mStartBufferedPosition = currentPosition + diff;
+ mLastReadPosition.set(0);
+ return 0;
}
return ret;
}
@@ -114,7 +126,10 @@ public class TunerTsStreamer implements TsStreamer {
*/
public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
mTunerHal = tunerHal;
- mEventDetector = new EventDetector(mTunerHal, eventListener);
+ mEventDetector = new EventDetector(mTunerHal);
+ if (eventListener != null) {
+ mEventDetector.registerListener(eventListener);
+ }
mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ?
new TsStreamWriter(context) : null;
}
@@ -125,7 +140,8 @@ public class TunerTsStreamer implements TsStreamer {
@Override
public boolean startStream(TunerChannel channel) {
- if (mTunerHal.tune(channel.getFrequency(), channel.getModulation())) {
+ if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(),
+ channel.getDisplayNumber(false))) {
if (channel.hasVideo()) {
mTunerHal.addPidFilter(channel.getVideoPid(),
TunerHal.FILTER_TYPE_VIDEO);
@@ -148,6 +164,7 @@ public class TunerTsStreamer implements TsStreamer {
channel.getProgramNumber());
}
mChannel = channel;
+ mChannelNumber = channel.getDisplayNumber();
synchronized (mCircularBufferMonitor) {
if (mStreaming) {
Log.w(TAG, "Streaming should be stopped before start streaming");
@@ -156,7 +173,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
if (mTsStreamWriter != null) {
mTsStreamWriter.setChannel(mChannel);
@@ -172,7 +188,7 @@ public class TunerTsStreamer implements TsStreamer {
@Override
public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
- if (mTunerHal.tune(channel.frequency, channel.modulation)) {
+ if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
mEventDetector.startDetecting(
channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
synchronized (mCircularBufferMonitor) {
@@ -183,7 +199,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
mStreamingThread = new StreamingThread();
mStreamingThread.start();
@@ -258,6 +273,22 @@ public class TunerTsStreamer implements TsStreamer {
}
}
+ public String getStreamerInfo() {
+ return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming;
+ }
+
+ public void registerListener(EventListener listener) {
+ if (mEventDetector != null && listener != null) {
+ mEventDetector.registerListener(listener);
+ }
+ }
+
+ public void unregisterListener(EventListener listener) {
+ if (mEventDetector != null) {
+ mEventDetector.unregisterListener(listener);
+ }
+ }
+
private class StreamingThread extends Thread {
@Override
public void run() {
@@ -321,21 +352,14 @@ public class TunerTsStreamer implements TsStreamer {
* @throws IOException
*/
public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
- long readStartTime = System.currentTimeMillis();
while (true) {
synchronized (mCircularBufferMonitor) {
- if (mEndOfStreamSent || !mStreaming) {
- return -1;
+ if (!mStreaming) {
+ return READ_ERROR_STREAMING_ENDED;
}
if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) {
- Log.e(TAG, "Demux is requesting the data which is already overwritten.");
- return -1;
- }
- if (System.currentTimeMillis() - readStartTime > READ_TIMEOUT_MS) {
- // Nothing was received during READ_TIMEOUT_MS before.
- mEndOfStreamSent = true;
- mCircularBufferMonitor.notifyAll();
- return -1;
+ Log.w(TAG, "Demux is requesting the data which is already overwritten.");
+ return READ_ERROR_BUFFER_OVERWRITTEN;
}
if (mBytesFetched < pos + amount) {
try {
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
index cf1f6dcf..fcd14116 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -42,6 +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<Integer, EventDetector.EventListener> mListeners = new HashMap<>();
private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
private final TunerHalManager mTunerHalManager = new TunerHalManager();
private static TunerTsStreamerManager sInstance;
@@ -68,6 +69,8 @@ class TunerTsStreamerManager {
mStreamerFinder.appendSessionLocked(channel, sessionId);
TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ streamer.registerListener(listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -83,6 +86,8 @@ class TunerTsStreamerManager {
if (!creator.isCancelledLocked()) {
mStreamerFinder.putLocked(channel, sessionId, streamer);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ streamer.registerListener(listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -104,6 +109,8 @@ class TunerTsStreamerManager {
if (streamer == null) {
return;
}
+ EventDetector.EventListener listener = mListeners.remove(sessionId);
+ streamer.unregisterListener(listener);
TunerChannel channel = streamer.getChannel();
SoftPreconditions.checkState(channel != null);
mStreamerFinder.removeSessionLocked(channel, sessionId);
@@ -125,6 +132,13 @@ class TunerTsStreamerManager {
}
}
+ /**
+ * Add tuner hal into TunerHalManager for test.
+ */
+ void addTunerHal(TunerHal tunerHal, int sessionId) {
+ mTunerHalManager.addTunerHal(tunerHal, sessionId);
+ }
+
synchronized void release(int sessionId) {
mTunerHalManager.releaseCachedHal(sessionId);
}
@@ -261,16 +275,16 @@ class TunerTsStreamerManager {
}
private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
- if (!reuse) {
+ if (!reuse || !hal.isReusable()) {
AutoCloseableUtils.closeQuietly(hal);
return;
}
TunerHal cachedHal = mTunerHals.get(sessionId);
if (cachedHal != hal) {
mTunerHals.put(sessionId, hal);
- }
- if (cachedHal != null && cachedHal != hal) {
- AutoCloseableUtils.closeQuietly(cachedHal);
+ if (cachedHal != null) {
+ AutoCloseableUtils.closeQuietly(cachedHal);
+ }
}
}
@@ -283,5 +297,9 @@ class TunerTsStreamerManager {
AutoCloseableUtils.closeQuietly(hal);
}
}
+
+ private void addTunerHal(TunerHal tunerHal, int sessionId) {
+ mTunerHals.put(sessionId, tunerHal);
+ }
}
} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java
index 8c1f6a1b..fe972cd1 100644
--- a/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/src/com/android/tv/tuner/ts/SectionParser.java
@@ -22,7 +22,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsiData.PatItem;
import com.android.tv.tuner.data.PsiData.PmtItem;
import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor;
@@ -39,8 +39,8 @@ import com.android.tv.tuner.data.PsipData.RatingRegion;
import com.android.tv.tuner.data.PsipData.RegionalRating;
import com.android.tv.tuner.data.PsipData.TsDescriptor;
import com.android.tv.tuner.data.PsipData.VctItem;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.util.ByteArrayBuffer;
import com.ibm.icu.text.UnicodeDecompressor;
@@ -367,6 +367,10 @@ public class SectionParser {
mParsedEttItems.clear();
}
+ public void resetVersionNumbers() {
+ mSectionVersionMap.clear();
+ }
+
private void parseSection(byte[] data) {
if (!checkSanity(data)) {
Log.d(TAG, "Bad CRC!");
@@ -510,10 +514,8 @@ public class SectionParser {
pos += 11 + descriptorsLength;
results.add(new MgtItem(tableType, tableTypePid));
}
- if ((data[pos] & 0xf0) != 0xf0) {
- Log.e(TAG, "Broken MGT.");
- return false;
- }
+ // Skip the remaining descriptor part which we don't use.
+
if (mListener != null) {
mListener.onMgtParsed(results);
}
@@ -717,6 +719,9 @@ public class SectionParser {
if (audioDescriptor.getLanguage() != null) {
audioTrack.language = audioDescriptor.getLanguage();
}
+ if (audioTrack.language == null) {
+ audioTrack.language = "";
+ }
audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED;
audioTrack.channelCount = audioDescriptor.getNumChannels();
audioTrack.sampleRate = audioDescriptor.getSampleRate();
@@ -948,6 +953,7 @@ public class SectionParser {
pos += 3;
boolean ccType = (data[pos] & 0x80) != 0;
if (!ccType) {
+ pos +=3;
continue;
}
int captionServiceNumber = data[pos] & 0x3f;
diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java
index c24c2a21..21b5a942 100644
--- a/src/com/android/tv/tuner/ts/TsParser.java
+++ b/src/com/android/tv/tuner/ts/TsParser.java
@@ -102,6 +102,7 @@ public class TsParser {
}
protected abstract void handleData(byte[] data, boolean startIndicator);
+ protected abstract void resetDataVersions();
}
private class SectionStream extends Stream {
@@ -138,6 +139,11 @@ public class TsParser {
mSectionParser.parseSections(mPacket);
}
+ @Override
+ protected void resetDataVersions() {
+ mSectionParser.resetVersionNumbers();
+ }
+
private final OutputListener mSectionListener = new OutputListener() {
@Override
public void onPatParsed(List<PatItem> items) {
@@ -451,4 +457,16 @@ public class TsParser {
}
return incompleteChannels;
}
+
+ /**
+ * Reset the versions so that data with old version number can be handled.
+ */
+ public void resetDataVersions() {
+ for (int eitPid : mEITPids) {
+ Stream stream = mStreamMap.get(eitPid);
+ if (stream != null) {
+ stream.resetDataVersions();
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
index a16bc522..885cef9f 100644
--- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -30,6 +30,7 @@ import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
import android.text.format.DateUtils;
import android.util.Log;
@@ -37,6 +38,7 @@ import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.ConvertUtils;
+import com.android.tv.util.PermissionUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -192,11 +194,14 @@ public class ChannelDataManager implements Handler.Callback {
public void release() {
mHandler.removeCallbacksAndMessages(null);
- mHandlerThread.quitSafely();
+ releaseSafely();
}
public void releaseSafely() {
mHandlerThread.quitSafely();
+ mListener = null;
+ mChannelScanListener = null;
+ mChannelScanHandler = null;
}
public TunerChannel getChannel(long channelId) {
@@ -435,7 +440,7 @@ public class ChannelDataManager implements Handler.Callback {
}
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
- TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId()));
+ TvContract.Programs.CONTENT_URI), newItem, channel));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
@@ -505,7 +510,7 @@ public class ChannelDataManager implements Handler.Callback {
continue;
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
- TvContract.Programs.CONTENT_URI), item, channel.getChannelId()));
+ TvContract.Programs.CONTENT_URI), item, channel));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
@@ -516,9 +521,13 @@ public class ChannelDataManager implements Handler.Callback {
}
private ContentProviderOperation buildContentProviderOperation(
- ContentProviderOperation.Builder builder, EitItem item, Long channelId) {
- if (channelId != null) {
- builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId);
+ ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) {
+ if (channel != null) {
+ builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId());
+ if (BuildCompat.isAtLeastN()) {
+ builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED,
+ channel.isRecordingProhibited() ? 1 : 0);
+ }
}
if (item != null) {
builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
@@ -556,7 +565,10 @@ public class ChannelDataManager implements Handler.Callback {
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray());
values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription());
+ values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION);
+ values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ channel.isRecordingProhibited() ? 1 : 0);
if (channelId <= 0) {
values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId);
@@ -598,13 +610,29 @@ public class ChannelDataManager implements Handler.Callback {
}
private void checkVersion() {
- String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
- try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
- CHANNEL_DATA_SELECTION_ARGS, selection,
- new String[] {Integer.toString(VERSION)}, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- // The stored channel data seem outdated. Delete them all.
- clearChannels();
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
+ try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
+ CHANNEL_DATA_SELECTION_ARGS, selection,
+ new String[] {Integer.toString(VERSION)}, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ // The stored channel data seem outdated. Delete them all.
+ clearChannels();
+ }
+ }
+ } else {
+ try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
+ new String[] { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 },
+ null, null, null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ int version = cursor.getInt(0);
+ if (version != VERSION) {
+ clearChannels();
+ break;
+ }
+ }
+ }
}
}
}
diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java
index a132398f..96b20a4b 100644
--- a/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/EventDetector.java
@@ -21,8 +21,8 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.ts.TsParser;
import com.android.tv.tuner.data.PsiData;
@@ -51,7 +51,7 @@ public class EventDetector {
private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>();
private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray();
private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray();
- private final EventListener mEventListener;
+ private final List<EventListener> mEventListeners = new ArrayList<>();
private int mFrequency;
private String mModulation;
private int mProgramNumber = ALL_PROGRAM_NUMBERS;
@@ -105,8 +105,10 @@ public class EventDetector {
item.setHasCaptionTrack();
}
}
- if (tunerChannel != null && mEventListener != null) {
- mEventListener.onEventDetected(tunerChannel, items);
+ if (tunerChannel != null && !mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onEventDetected(tunerChannel, items);
+ }
}
}
@@ -117,8 +119,10 @@ public class EventDetector {
@Override
public void onAllVctItemsParsed() {
- if (mEventListener != null) {
- mEventListener.onChannelScanDone();
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelScanDone();
+ }
}
}
@@ -161,8 +165,10 @@ public class EventDetector {
if (!found) {
mVctProgramNumberSet.add(channelProgramNumber);
}
- if (mEventListener != null) {
- mEventListener.onChannelDetected(tunerChannel, !found);
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelDetected(tunerChannel, !found);
+ }
}
}
};
@@ -197,11 +203,9 @@ public class EventDetector {
/**
* Creates a detector for ATSC TV channles and program information.
* @param usbTunerInteface {@link TunerHal}
- * @param listener for ATSC TV channels and program information
*/
- public EventDetector(TunerHal usbTunerInteface, EventListener listener) {
+ public EventDetector(TunerHal usbTunerInteface) {
mTunerHal = usbTunerInteface;
- mEventListener = listener;
}
private void reset() {
@@ -258,4 +262,28 @@ public class EventDetector {
public List<TunerChannel> getMalFormedChannels() {
return mTsParser.getMalFormedChannels();
}
+
+ /**
+ * Registers an EventListener.
+ * @param eventListener the listener to be registered
+ */
+ public void registerListener(EventListener eventListener) {
+ if (mTsParser != null) {
+ // Resets the version numbers so that the new listener can receive the EIT items.
+ // Otherwise, each EIT session is handled only once unless there is a new version.
+ mTsParser.resetDataVersions();
+ }
+ mEventListeners.add(eventListener);
+ }
+
+ /**
+ * Unregisters an EventListener.
+ * @param eventListener the listener to be unregistered
+ */
+ public void unregisterListener(EventListener eventListener) {
+ boolean removed = mEventListeners.remove(eventListener);
+ if (!removed && DEBUG) {
+ Log.d(TAG, "Cannot unregister a non-registered listener!");
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
index 61de24f4..46ff4ea1 100644
--- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
@@ -24,8 +24,8 @@ import com.android.tv.tuner.data.PsiData.PatItem;
import com.android.tv.tuner.data.PsiData.PmtItem;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.VctItem;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.source.FileTsStreamer;
import com.android.tv.tuner.ts.TsParser;
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index 6ec55e4f..0be29f25 100644
--- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -33,13 +33,17 @@ import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer.C;
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.dvr.data.RecordedProgram;
import com.android.tv.tuner.DvbDeviceAccessor;
import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
import com.android.tv.tuner.exoplayer.SampleExtractor;
@@ -53,10 +57,10 @@ 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.Locale;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
@@ -71,6 +75,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS
+ ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", "
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
+ private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
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;
@@ -80,20 +85,23 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
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 static final int MSG_UPDATE_CC_INFO = 7;
private final RecordingCapability mCapabilities;
public RecordingCapability getCapabilities() {
return mCapabilities;
}
- @IntDef({STATE_IDLE, STATE_TUNED, STATE_RECORDING})
+ @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface DvrSessionState {}
private static final int STATE_IDLE = 1;
- private static final int STATE_TUNED = 2;
- private static final int STATE_RECORDING = 3;
+ private static final int STATE_TUNING = 2;
+ private static final int STATE_TUNED = 3;
+ private static final int STATE_RECORDING = 4;
private static final long CHANNEL_ID_NONE = -1;
+ private static final int MAX_TUNING_RETRY = 6;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
@@ -108,13 +116,16 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private long mRecordStartTime;
private long mRecordEndTime;
private boolean mRecorderRunning;
- private BufferManager mBufferManager;
private SampleExtractor mRecorder;
private final TunerRecordingSession mSession;
@DvrSessionState private int mSessionState = STATE_IDLE;
private final String mInputId;
private Uri mProgramUri;
+ private PsipData.EitItem mCurrenProgram;
+ private List<AtscCaptionTrack> mCaptionTracks;
+ private DvrStorageManager mDvrStorageManager;
+
public TunerRecordingSessionWorker(Context context, String inputId,
ChannelDataManager dataManager, TunerRecordingSession session) {
mRandom.setSeed(System.nanoTime());
@@ -157,6 +168,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
if (mChannel == null || mChannel.compareTo(channel) != 0) {
return;
}
+ mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
mChannelDataManager.notifyEventDetected(channel, items);
}
@@ -178,7 +190,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
@MainThread
public void tune(Uri channelUri) {
mHandler.removeCallbacksAndMessages(null);
- mHandler.obtainMessage(MSG_TUNE, channelUri).sendToTarget();
+ mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
}
/**
@@ -211,11 +223,22 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
switch (msg.what) {
case MSG_TUNE: {
Uri channelUri = (Uri) msg.obj;
+ int retryCount = msg.arg1;
if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
if (doTune(channelUri)) {
- mSession.onTuned(channelUri);
- } else {
- reset();
+ if (mSessionState == STATE_TUNED) {
+ mSession.onTuned(channelUri);
+ } else {
+ Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
+ if (retryCount < MAX_TUNING_RETRY) {
+ Message tuneMsg =
+ mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri);
+ mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
+ } else {
+ mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
+ reset();
+ }
+ }
}
return true;
}
@@ -281,6 +304,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mHandler.getLooper().quitSafely();
return true;
}
+ case MSG_UPDATE_CC_INFO: {
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ updateCaptionTracks(pair.first, pair.second);
+ return true;
+ }
}
return false;
}
@@ -310,20 +339,17 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mRecorder.release();
mRecorder = null;
}
- if (mBufferManager != null) {
- mBufferManager.close();
- mBufferManager = null;
- }
if (mTunerSource != null) {
mSourceManager.releaseDataSource(mTunerSource);
mTunerSource = null;
}
+ mDvrStorageManager = null;
mSessionState = STATE_IDLE;
mRecorderRunning = false;
}
private boolean doTune(Uri channelUri) {
- if (mSessionState != STATE_IDLE) {
+ if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Tuning was requested from wrong status.");
return false;
@@ -333,6 +359,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
return false;
+ } else if (mChannel.isRecordingProhibited()) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
+ return false;
}
if (!mDvrStorageStatusManager.isStorageSufficient()) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
@@ -341,9 +371,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
if (mTunerSource == null) {
- mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
- Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
- return false;
+ // Retry tuning in this case.
+ mSessionState = STATE_TUNING;
+ return true;
}
mSessionState = STATE_TUNED;
return true;
@@ -365,10 +395,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
// Since tuning might be happened a while ago, shifts the start position of tuned source.
mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
- mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true));
mRecordStartTime = System.currentTimeMillis();
- mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this,
- true);
+ mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
+ mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource,
+ new BufferManager(mDvrStorageManager), this, true);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
mSessionState = STATE_RECORDING;
@@ -392,6 +422,34 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Log.i(TAG, "Recording stopped");
}
+ private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
+ if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0
+ || items == null || items.isEmpty()) {
+ return;
+ }
+ PsipData.EitItem currentProgram = getCurrentProgram(items);
+ if (currentProgram == null || !currentProgram.hasCaptionTrack()
+ || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) {
+ return;
+ }
+ mCurrenProgram = currentProgram;
+ mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
+ if (DEBUG) {
+ Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for "
+ + currentProgram);
+ }
+ }
+
+ private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
+ for (PsipData.EitItem item : items) {
+ if (mRecordStartTime >= item.getStartTimeUtcMillis()
+ && mRecordStartTime < item.getEndTimeUtcMillis()) {
+ return item;
+ }
+ }
+ return null;
+ }
+
private static class Program {
private final long mChannelId;
private final String mTitle;
@@ -566,15 +624,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
return;
}
Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
- Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(),
- Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime,
- mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs));
+ long recordEndTime =
+ (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
+ ? System.currentTimeMillis()
+ : mRecordStartTime + lastExtractedPositionUs / 1000;
+ Uri uri =
+ insertRecordedProgram(
+ getRecordedProgram(),
+ mChannel.getChannelId(),
+ Uri.fromFile(mStorageDir).toString(),
+ 1024 * 1024,
+ mRecordStartTime,
+ recordEndTime);
if (uri == null) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Inserting a recording to DB failed");
return;
}
+ mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
mSession.onRecordFinished(uri);
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java
index 5c61402e..d1ee3c6f 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -41,7 +41,7 @@ import com.android.tv.tuner.R;
import com.android.tv.tuner.cc.CaptionLayout;
import com.android.tv.tuner.cc.CaptionTrackRenderer;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+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;
@@ -81,8 +81,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
private boolean mPlayPaused;
private long mTuneStartTimestamp;
- public TunerSession(Context context, ChannelDataManager channelDataManager,
- BufferManager bufferManager) {
+ public TunerSession(Context context, ChannelDataManager channelDataManager) {
super(context);
mContext = context;
mUiHandler = new Handler(this);
@@ -97,12 +96,9 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
mAudioStatusView.setVisibility(View.INVISIBLE);
- mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
- 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,
- bufferManager, this);
+ mSessionWorker = new TunerSessionWorker(context, channelDataManager, this);
}
public boolean isReleased() {
@@ -272,10 +268,13 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
// setting is "never".
final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
- mAudioStatusView.setVisibility(View.VISIBLE);
+ mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(R.string.ut_surround_sound_disabled))));
} else {
- Log.e(TAG, "Audio is unavailable, surround sound setting is " + value);
+ mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(R.string.audio_passthrough_not_supported))));
}
+ mAudioStatusView.setVisibility(View.VISIBLE);
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 5230298e..41f8ce5f 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -35,6 +35,7 @@ import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
import android.text.Html;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -47,28 +48,30 @@ 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;
import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.util.StatusTextUtils;
+import com.android.tv.tuner.util.SystemPropertiesProxy;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
/**
* {@link TunerSessionWorker} implements a handler thread which processes TV input jobs
@@ -82,6 +85,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private static final boolean DEBUG = false;
private static final boolean ENABLE_PROFILER = true;
private static final String PLAY_FROM_CHANNEL = "channel";
+ 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
// Public messages
public static final int MSG_SELECT_TRACK = 1;
@@ -147,10 +153,18 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500;
private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20;
private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
+ private static final int RELEASE_WAIT_INTERVAL_MS = 50;
+
+ // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker
+ // creation/release is required.
+ // This is used to guarantee that at most one active TunerSessionWorker exists at any give time.
+ private static Semaphore sActiveSessionSemaphore = new Semaphore(1);
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
private final TsDataSourceManager mSourceManager;
+ private final int mMaxTrickplayBufferSizeMb;
+ private final File mTrickplayBufferDir;
private volatile Surface mSurface;
private volatile float mVolume = 1.0f;
private volatile boolean mCaptionEnabled;
@@ -159,6 +173,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private volatile Long mRecordingDuration;
private volatile long mRecordStartTimeMs;
private volatile long mBufferStartTimeMs;
+ private volatile boolean mTrickplayDisabled;
private String mRecordingId;
private final Handler mHandler;
private int mRetryCount;
@@ -177,19 +192,19 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private TvContentRating mUnblockedContentRating;
private long mLastPositionMs;
private AudioCapabilities mAudioCapabilities;
- private final CountDownLatch mReleaseLatch = new CountDownLatch(1);
private long mLastLimitInBytes;
- private long mLastPositionInBytes;
- private final BufferManager mBufferManager;
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;
+ private boolean mIsActiveSession;
+ private boolean mReleaseRequested; // Guarded by mReleaseLock
+ private final Object mReleaseLock = new Object();
public TunerSessionWorker(Context context, ChannelDataManager channelDataManager,
- BufferManager bufferManager, TunerSession tunerSession) {
+ TunerSession tunerSession) {
if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
mContext = context;
@@ -211,7 +226,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
(CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
mCaptionEnabled = captioningManager.isEnabled();
mPlaybackParams.setSpeed(1.0f);
- mBufferManager = bufferManager;
+ mMaxTrickplayBufferSizeMb =
+ SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+ mTrickplayBufferDir = context.getCacheDir();
+ mTrickplayDisabled = mTrickplayBufferDir == null;
mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
@@ -285,24 +303,21 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private Long getDurationForRecording(String recordingId) {
- try {
- DvrStorageManager storageManager =
+ DvrStorageManager storageManager =
new DvrStorageManager(new File(getRecordingPath()), false);
- Pair<String, MediaFormat> trackInfo = null;
- try {
- trackInfo = storageManager.readTrackInfoFile(false);
- } catch (FileNotFoundException e) {
- }
- if (trackInfo == null) {
- trackInfo = storageManager.readTrackInfoFile(true);
- }
- Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION);
+ List<BufferManager.TrackFormat> trackFormatList =
+ storageManager.readTrackInfoFiles(false);
+ if (trackFormatList.isEmpty()) {
+ trackFormatList = storageManager.readTrackInfoFiles(true);
+ }
+ if (!trackFormatList.isEmpty()) {
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(0);
+ Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION);
// we need duration by milli for trickplay notification.
return durationUs != null ? durationUs / 1000 : null;
- } catch (IOException e) {
- Log.e(TAG, "meta file for recording was not found: " + recordingId);
- return null;
}
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
}
@MainThread
@@ -341,16 +356,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@MainThread
public void release() {
if (DEBUG) Log.d(TAG, "release()");
+ synchronized (mReleaseLock) {
+ mReleaseRequested = true;
+ }
mChannelDataManager.setListener(null);
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();
- }
}
// MpegTsPlayer.Listener
@@ -367,7 +378,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (playbackState == ExoPlayer.STATE_READY) {
if (DEBUG) Log.d(TAG, "ExoPlayer ready");
if (!mPlayerStarted) {
- sendMessage(MSG_START_PLAYBACK, mPlayer);
+ sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer));
}
mReadyStartTimeMs = SystemClock.elapsedRealtime();
} else if (playbackState == ExoPlayer.STATE_PREPARING) {
@@ -379,7 +390,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
Log.i(TAG, "Player ended: end of stream");
if (mChannel != null) {
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
}
}
mPlayerState = playbackState;
@@ -397,7 +408,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
// retrying playback is not helpful.
if (mChannel != null) {
- mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget();
+ mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer))
+ .sendToTarget();
}
}
@@ -415,8 +427,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
if (mSurface != null && mPlayerStarted) {
if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
- mBufferStartTimeMs = mRecordStartTimeMs =
- (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
notifyVideoAvailable();
mReportedDrawnToSurface = true;
@@ -499,7 +515,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@Override
public void onDiskTooSlow() {
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ mTrickplayDisabled = true;
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
}
// EventDetector.EventListener
@@ -602,6 +619,28 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ if (!mIsActiveSession) {
+ // Wait until release is finished if there is a pending release.
+ try {
+ while (!sActiveSessionSemaphore.tryAcquire(
+ RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ return true;
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ sActiveSessionSemaphore.release();
+ return true;
+ }
+ }
+ mIsActiveSession = true;
+ }
Uri channelUri = (Uri) msg.obj;
String recording = null;
long channelId = parseChannel(channelUri);
@@ -616,7 +655,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return true;
}
- mHandler.removeCallbacksAndMessages(null);
+ clearCallbacksAndMessagesSafely();
+ mChannelDataManager.removeAllCallbacksAndMessages();
if (channel != null) {
mChannelDataManager.requestProgramsData(channel);
}
@@ -624,8 +664,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// TODO: Need to refactor. notifyContentAllowed() should not be called if parental
// control is turned on.
mSession.notifyContentAllowed();
- resetPlayback();
resetTvTracks();
+ resetPlayback();
mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
return true;
@@ -633,7 +673,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
case MSG_STOP_TUNE: {
if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
mChannel = null;
- stopPlayback();
+ stopPlayback(true);
stopCaptionTrack();
resetTvTracks();
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
@@ -642,14 +682,17 @@ public class TunerSessionWorker implements PlaybackBufferListener,
case MSG_RELEASE: {
if (DEBUG) Log.d(TAG, "MSG_RELEASE");
mHandler.removeCallbacksAndMessages(null);
- stopPlayback();
+ stopPlayback(true);
stopCaptionTrack();
mSourceManager.release();
- mReleaseLatch.countDown();
+ mHandler.getLooper().quitSafely();
+ if (mIsActiveSession) {
+ sActiveSessionSemaphore.release();
+ }
return true;
}
case MSG_RETRY_PLAYBACK: {
- if (mPlayer == msg.obj) {
+ if (System.identityHashCode(mPlayer) == (int) msg.obj) {
Log.i(TAG, "Retrying the playback for channel: " + mChannel);
mHandler.removeMessages(MSG_RETRY_PLAYBACK);
// When there is a request of retrying playback, don't reuse TunerHal.
@@ -658,13 +701,14 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) {
Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
}
+ mChannelDataManager.removeAllCallbacksAndMessages();
if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
resetPlayback();
} else {
// When it reaches this point, it may be due to an error that occurred in
// the tuner device. Calling stopPlayback() resets the tuner device
// to recover from the error.
- stopPlayback();
+ stopPlayback(false);
stopCaptionTrack();
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
@@ -679,13 +723,14 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
case MSG_RESET_PLAYBACK: {
if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK");
+ mChannelDataManager.removeAllCallbacksAndMessages();
resetPlayback();
return true;
}
case MSG_START_PLAYBACK: {
if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK");
if (mChannel != null || mRecordingId != null) {
- startPlayback(msg.obj);
+ startPlayback((int) msg.obj);
}
return true;
}
@@ -790,7 +835,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_RESCHEDULE_PROGRAMS: {
- doReschedulePrograms();
+ if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+ } else {
+ doReschedulePrograms();
+ }
return true;
}
case MSG_PARENTAL_CONTROLS: {
@@ -814,11 +863,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_SELECT_TRACK: {
- if (mChannel != null) {
+ if (mChannel != null || mRecordingId != null) {
doSelectTrack(msg.arg1, (String) msg.obj);
- } else if (mRecordingId != null) {
- // TODO : mChannel == null && mRecordingId != null
- Log.d(TAG, "track selected for recording");
}
return true;
}
@@ -909,7 +955,6 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
TsDataSource source = mPlayer.getDataSource();
long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
- long positionInBytes = source != null ? source.getLastReadPosition() : 0L;
if (TunerDebug.ENABLED) {
TunerDebug.calculateDiff();
mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT,
@@ -927,14 +972,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
TunerDebug.getVideoPtsUsRate()
)));
}
- if (DEBUG) {
- Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d",
- positionInBytes, limitInBytes));
- }
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
long currentTime = SystemClock.elapsedRealtime();
- boolean noBufferRead = positionInBytes == mLastPositionInBytes
- && limitInBytes == mLastLimitInBytes;
boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME
&& currentTime - mBufferingStartTimeMs
> PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
@@ -943,11 +982,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
> PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
boolean isWeakSignal = source != null
&& mChannel.getType() == Channel.TYPE_TUNER
- && (noBufferRead || isBufferingTooLong || isPreparingTooLong);
+ && (isBufferingTooLong || isPreparingTooLong);
if (isWeakSignal && !mReportedWeakSignal) {
if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK,
+ System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS);
}
if (mPlayer != null) {
mPlayer.setAudioTrack(false);
@@ -966,7 +1005,6 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
mLastLimitInBytes = limitInBytes;
- mLastPositionInBytes = positionInBytes;
mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
return true;
}
@@ -999,15 +1037,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (trackId == null) {
return;
}
- AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId);
- if (audioTrack == null) {
- return;
- }
- int oldAudioPid = mChannel.getAudioPid();
- mChannel.selectAudioTrack(audioTrack.index);
- int newAudioPid = mChannel.getAudioPid();
- if (oldAudioPid != newAudioPid) {
- mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index);
+ if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) {
+ mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId);
}
mSession.notifyTrackSelected(type, trackId);
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
@@ -1030,11 +1061,22 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) {
+ private MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
if (capabilities == null) {
Log.w(TAG, "No Audio Capabilities");
}
-
+ BufferManager bufferManager = null;
+ if (mRecordingId != null) {
+ StorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ bufferManager = new BufferManager(storageManager);
+ updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles());
+ } else if (!mTrickplayDisabled && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
+ bufferManager = new BufferManager(new TrickplayStorageManager(mContext,
+ mTrickplayBufferDir, 1024L * 1024 * mMaxTrickplayBufferSizeMb));
+ } else {
+ Log.w(TAG, "Trickplay is disabled.");
+ }
MpegTsPlayer player = new MpegTsPlayer(
new MpegTsRendererBuilder(mContext, bufferManager, this),
mHandler, mSourceManager, capabilities, this);
@@ -1069,24 +1111,26 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
- if (DEBUG) {
- Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
- }
- List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
- List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
- // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
- // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
- // track info in PMT more and use info in EIT only when we have nothing.
- if (audioTracks != null && !audioTracks.isEmpty()
- && (mChannel.getAudioTracks() == null || fromPmt)) {
- updateAudioTracks(audioTracks);
- }
- if (captionTracks == null || captionTracks.isEmpty()) {
- if (tvTracksInterface.hasCaptionTrack()) {
+ synchronized (tvTracksInterface) {
+ if (DEBUG) {
+ Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+ }
+ List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+ List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+ // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
+ // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
+ // track info in PMT more and use info in EIT only when we have nothing.
+ if (audioTracks != null && !audioTracks.isEmpty()
+ && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) {
+ updateAudioTracks(audioTracks);
+ }
+ if (captionTracks == null || captionTracks.isEmpty()) {
+ if (tvTracksInterface.hasCaptionTrack()) {
+ updateCaptionTracks(captionTracks);
+ }
+ } else {
updateCaptionTracks(captionTracks);
}
- } else {
- updateCaptionTracks(captionTracks);
}
}
@@ -1132,25 +1176,24 @@ public class TunerSessionWorker implements PlaybackBufferListener,
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;
+ // We use language information from EIT/VCT only when the player does not provide
+ // languages.
+ com.google.android.exoplayer.MediaFormat infoFromPlayer =
+ mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i);
+ AtscAudioTrack infoFromEit = mAudioTrackMap.get(i);
+ AtscAudioTrack infoFromVct = (mChannel != null
+ && mChannel.getAudioTracks().size() == mAudioTrackMap.size()
+ && i < mChannel.getAudioTracks().size())
+ ? mChannel.getAudioTracks().get(i) : null;
+ String language = !TextUtils.isEmpty(infoFromPlayer.language) ? infoFromPlayer.language
+ : (infoFromEit != null && infoFromEit.language != null) ? infoFromEit.language
+ : (infoFromVct != null && infoFromVct.language != null)
+ ? infoFromVct.language : null;
TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
builder.setLanguage(language);
- builder.setAudioChannelCount(audioTrack.channelCount);
- builder.setAudioSampleRate(audioTrack.sampleRate);
+ builder.setAudioChannelCount(infoFromPlayer.channelCount);
+ builder.setAudioSampleRate(infoFromPlayer.sampleRate);
TvTrackInfo track = builder.build();
mTvTracks.add(track);
}
@@ -1226,8 +1269,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private void stopPlayback() {
- mChannelDataManager.removeAllCallbacksAndMessages();
+ private void stopPlayback(boolean removeChannelDataCallbacks) {
+ if (removeChannelDataCallbacks) {
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ }
if (mPlayer != null) {
mPlayer.setPlayWhenReady(false);
mPlayer.release();
@@ -1244,9 +1289,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private void startPlayback(Object playerObj) {
+ private void startPlayback(int playerHashCode) {
// TODO: provide hasAudio()/hasVideo() for play recordings.
- if (mPlayer == null || mPlayer != playerObj) {
+ if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) {
return;
}
if (mChannel != null && !mChannel.hasAudio()) {
@@ -1259,7 +1304,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio())
|| (mChannel.hasVideo() && !mPlayer.hasVideo()))) {
// Tracks haven't been detected in the extractor. Try again.
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
return;
}
// Since mSurface is volatile, we define a local variable surface to keep the same value
@@ -1286,9 +1331,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return;
}
mSourceManager.setKeepTuneStatus(true);
- BufferManager bufferManager = mChannel != null ? mBufferManager : new BufferManager(
- new DvrStorageManager(new File(getRecordingPath()), false));
- MpegTsPlayer player = createPlayer(mAudioCapabilities, bufferManager);
+ MpegTsPlayer player = createPlayer(mAudioCapabilities);
player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
player.setVideoEventListener(this);
player.setCaptionServiceNumber(mCaptionTrack != null ?
@@ -1300,8 +1343,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// 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);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK,
+ System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS);
}
} else {
mPlayer = player;
@@ -1314,7 +1357,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private void resetPlayback() {
long timestamp, oldTimestamp;
timestamp = SystemClock.elapsedRealtime();
- stopPlayback();
+ stopPlayback(false);
stopCaptionTrack();
if (ENABLE_PROFILER) {
oldTimestamp = timestamp;
@@ -1336,8 +1379,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
mProgram = null;
mPrograms = null;
- mBufferStartTimeMs = mRecordStartTimeMs =
- (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
mLastPositionMs = 0;
mCaptionTrack = null;
mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
@@ -1544,15 +1591,15 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
mChannelBlocked = channelBlocked;
if (mChannelBlocked) {
- mHandler.removeCallbacksAndMessages(null);
- stopPlayback();
+ clearCallbacksAndMessagesSafely();
+ stopPlayback(true);
resetTvTracks();
if (contentRating != null) {
mSession.notifyContentBlocked(contentRating);
}
mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
} else {
- mHandler.removeCallbacksAndMessages(null);
+ clearCallbacksAndMessagesSafely();
resetPlayback();
mSession.notifyContentAllowed();
mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
@@ -1562,6 +1609,17 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
+ @WorkerThread
+ private void clearCallbacksAndMessagesSafely() {
+ // If MSG_RELEASE is removed, TunerSessionWorker will hang forever.
+ // Do not remove messages, after release is requested from MainThread.
+ synchronized (mReleaseLock) {
+ if (!mReleaseRequested) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+ }
+
private boolean hasEnoughBackwardBuffer() {
return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
>= mBufferStartTimeMs - mRecordStartTimeMs;
diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
index 684ebdbd..6594e089 100644
--- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
+++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
@@ -28,9 +28,6 @@ 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;
import java.util.Collections;
import java.util.Set;
@@ -45,9 +42,6 @@ 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}
@@ -55,7 +49,6 @@ public class TunerTvInputService extends TvInputService
private ChannelDataManager mChannelDataManager;
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private AudioCapabilities mAudioCapabilities;
- private BufferManager mBufferManager;
@Override
public void onCreate() {
@@ -65,7 +58,6 @@ public class TunerTvInputService extends TvInputService
mChannelDataManager = new ChannelDataManager(getApplicationContext());
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
mAudioCapabilitiesReceiver.register();
- mBufferManager = createBufferManager();
if (CommonFeatures.DVR.isEnabled(this)) {
JobScheduler jobScheduler =
(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -79,11 +71,6 @@ public class TunerTvInputService extends TvInputService
jobScheduler.schedule(job);
}
}
- if (mBufferManager == null) {
- Log.i(TAG, "Trickplay is disabled");
- } else {
- Log.i(TAG, "Trickplay is enabled");
- }
}
@Override
@@ -92,9 +79,6 @@ public class TunerTvInputService extends TvInputService
super.onDestroy();
mChannelDataManager.release();
mAudioCapabilitiesReceiver.unregister();
- if (mBufferManager != null) {
- mBufferManager.close();
- }
}
@Override
@@ -106,8 +90,7 @@ public class TunerTvInputService extends TvInputService
public Session onCreateSession(String inputId) {
if (DEBUG) Log.d(TAG, "onCreateSession");
try {
- final TunerSession session = new TunerSession(
- this, mChannelDataManager, mBufferManager);
+ final TunerSession session = new TunerSession(this, mChannelDataManager);
mTunerSessions.add(session);
session.setAudioCapabilities(mAudioCapabilities);
session.setOverlayViewEnabled(true);
@@ -129,17 +112,6 @@ public class TunerTvInputService extends TvInputService
}
}
- private BufferManager createBufferManager() {
- int maxBufferSizeMb =
- SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
- if (maxBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
- return new BufferManager(
- new TrickplayStorageManager(getApplicationContext(), getCacheDir(),
- 1024L * 1024 * maxBufferSizeMb));
- }
- return null;
- }
-
public static String getInputId(Context context) {
return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class));
}
diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java
new file mode 100644
index 00000000..3942ce95
--- /dev/null
+++ b/src/com/android/tv/tuner/util/PostalCodeUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.tuner.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.util.LocationUtils;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * A utility class to update, get, and set the last known postal or zip code.
+ */
+public class PostalCodeUtils {
+ private static final String TAG = "PostalCodeUtils";
+ private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
+
+ /** Returns {@code true} if postal code has been changed */
+ public static boolean updatePostalCode(Context context)
+ throws IOException, SecurityException, NoPostalCodeException {
+ String postalCode = getPostalCode(context);
+ String lastPostalCode = getLastPostalCode(context);
+ if (TextUtils.isEmpty(postalCode)) {
+ if (TextUtils.isEmpty(lastPostalCode)) {
+ throw new NoPostalCodeException();
+ }
+ } else if (!TextUtils.equals(postalCode, lastPostalCode)) {
+ setLastPostalCode(context, postalCode);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or
+ * input by users.
+ */
+ public static String getLastPostalCode(Context context) {
+ return TunerPreferences.getLastPostalCode(context);
+ }
+
+ /**
+ * Sets the last stored postal or zip code. This method will overwrite the value written by
+ * calling {@link #updatePostalCode(Context)}.
+ */
+ public static void setLastPostalCode(Context context, String postalCode) {
+ Log.i(TAG, "Set Postal Code:" + postalCode);
+ TunerPreferences.setLastPostalCode(context, postalCode);
+ }
+
+ @Nullable
+ private static String getPostalCode(Context context) throws IOException, SecurityException {
+ Address address = LocationUtils.getCurrentAddress(context);
+ if (address != null) {
+ Log.i(TAG, "Current country and postal code is " + address.getCountryName() + ", "
+ + address.getPostalCode());
+ if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
+ return address.getPostalCode();
+ }
+ }
+ return null;
+ }
+
+ /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */
+ public static class NoPostalCodeException extends Exception {
+ public NoPostalCodeException() {
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
index 62a64361..2817ccbf 100644
--- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
+++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
@@ -58,4 +58,20 @@ public class SystemPropertiesProxy {
}
return def;
}
+
+ public static String getString(String key, String def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class);
+ getIntMethod.setAccessible(true);
+ return (String) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.get()", e);
+ }
+ return def;
+ }
}
diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
index 5c411f64..fd9ec77d 100644
--- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
+++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
@@ -25,6 +25,7 @@ import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.os.BuildCompat;
import android.util.Log;
+import android.util.Pair;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.tuner.R;
@@ -43,23 +44,31 @@ public class TunerInputInfoUtils {
*/
@Nullable
@TargetApi(Build.VERSION_CODES.N)
- public static TvInputInfo buildTunerInputInfo(Context context, boolean fromBuiltInTuner) {
- int numOfDevices = TunerHal.getTunerCount(context);
- if (numOfDevices == 0) {
+ public static TvInputInfo buildTunerInputInfo(Context context) {
+ Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
+ if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
return null;
}
- TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context,
- TunerTvInputService.class));
- if (fromBuiltInTuner) {
- builder.setLabel(R.string.bt_app_name);
- } else {
- builder.setLabel(R.string.ut_app_name);
+ int inputLabelId = 0;
+ switch (tunerTypeAndCount.first) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ inputLabelId = R.string.bt_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ inputLabelId = R.string.ut_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ inputLabelId = R.string.nt_app_name;
+ break;
}
try {
- return builder.setCanRecord(CommonFeatures.DVR.isEnabled(context))
- .setTunerCount(numOfDevices)
+ TvInputInfo.Builder builder = new TvInputInfo.Builder(context,
+ new ComponentName(context, TunerTvInputService.class));
+ return builder.setLabel(inputLabelId)
+ .setCanRecord(CommonFeatures.DVR.isEnabled(context))
+ .setTunerCount(tunerTypeAndCount.second)
.build();
- } catch (NullPointerException e) {
+ } catch (IllegalArgumentException | NullPointerException e) {
// TunerTvInputService is not enabled.
return null;
}
@@ -73,7 +82,7 @@ public class TunerInputInfoUtils {
public static void updateTunerInputInfo(Context context) {
if (BuildCompat.isAtLeastN()) {
if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
- TvInputInfo info = buildTunerInputInfo(context, isBuiltInTuner(context));
+ TvInputInfo info = buildTunerInputInfo(context);
if (info != null) {
((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE))
.updateTvInputInfo(info);
@@ -88,13 +97,4 @@ public class TunerInputInfoUtils {
}
}
}
-
- /**
- * Returns if the current tuner service is for a built-in tuner.
- *
- * @param context {@link Context} instance
- */
- public static boolean isBuiltInTuner(Context context) {
- return TunerHal.getTunerType(context) == TunerHal.TUNER_TYPE_BUILT_IN;
- }
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java
index 09acb36b..6a83c541 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -22,6 +22,7 @@ import android.util.AttributeSet;
import android.view.SurfaceView;
import android.view.View;
+import com.android.tv.util.Debug;
import com.android.tv.experiments.Experiments;
/**
@@ -59,4 +60,13 @@ public class AppLayerTvView extends TvView {
}
super.onViewAdded(child);
}
+
+ @Override
+ public void getLocationOnScreen(int[] outLocation) {
+ super.getLocationOnScreen(outLocation);
+
+ // The TvView.MySessionCallback.onSessionCreated() will call this method indirectly.
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "AppLayerTvView.getLocationOnScreen, session created");
+ }
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 3cf4de83..eed536a8 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -57,7 +57,7 @@ import com.android.tv.data.Channel;
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.dvr.data.ScheduledRecording;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.util.ImageCache;
import com.android.tv.util.ImageLoader;
@@ -98,8 +98,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private static final String EMPTY_STRING = "";
- private static Program sNoProgram;
- private static Program sLockedChannelProgram;
+ private Program mNoProgram;
+ private Program mLockedChannelProgram;
private static String sClosedCaptionMark;
private final MainActivity mMainActivity;
@@ -123,6 +123,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private String mProgramDescriptionText;
private View mAnchorView;
private Channel mCurrentChannel;
+ private boolean mCurrentChannelLogoExists;
private Program mLastUpdatedProgram;
private final Handler mHandler = new Handler();
private final DvrManager mDvrManager;
@@ -130,6 +131,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private TvContentRating mBlockingContentRating;
private int mLockType;
+ private boolean mUpdateOnTune;
private Animator mResizeAnimator;
private int mCurrentHeight;
@@ -192,7 +194,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
@Override
public void run() {
removeCallbacks(this);
- updateViews(null);
+ updateViews(false);
}
};
@@ -243,18 +245,14 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mContentRatingsManager = TvApplication.getSingletons(getContext())
.getTvInputManagerHelper().getContentRatingsManager();
- if (sNoProgram == null) {
- sNoProgram = new Program.Builder()
- .setTitle(context.getString(R.string.channel_banner_no_title))
- .setDescription(EMPTY_STRING)
- .build();
- }
- if (sLockedChannelProgram == null){
- sLockedChannelProgram = new Program.Builder()
- .setTitle(context.getString(R.string.channel_banner_locked_channel_title))
- .setDescription(EMPTY_STRING)
- .build();
- }
+ mNoProgram = new Program.Builder()
+ .setTitle(context.getString(R.string.channel_banner_no_title))
+ .setDescription(EMPTY_STRING)
+ .build();
+ mLockedChannelProgram = new Program.Builder()
+ .setTitle(context.getString(R.string.channel_banner_locked_channel_title))
+ .setDescription(EMPTY_STRING)
+ .build();
if (sClosedCaptionMark == null) {
sClosedCaptionMark = context.getString(R.string.closed_caption);
}
@@ -345,19 +343,17 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
* Set new lock type.
*
* @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO.
- * @return {@code true} only if lock type is changed
+ * @return the previous lock type of the channel banner.
* @throws IllegalArgumentException if lockType is invalid.
*/
- public boolean setLockType(int lockType) {
+ public int setLockType(int lockType) {
if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO
&& lockType != LOCK_PROGRAM_DETAIL) {
throw new IllegalArgumentException("No such lock type " + lockType);
}
- if (mLockType != lockType) {
- mLockType = lockType;
- return true;
- }
- return false;
+ int previousLockType = mLockType;
+ mLockType = lockType;
+ return previousLockType;
}
/**
@@ -372,31 +368,34 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
/**
* Update channel banner view.
*
- * @param info A StreamInfo that includes stream information.
- * If it's {@code null}, only program information will be updated.
+ * @param updateOnTune {@false} denotes the channel banner is updated due to other reasons than
+ * tuning. The channel info will not be updated in this case.
*/
- public void updateViews(StreamInfo info) {
+ public void updateViews(boolean updateOnTune) {
resetAnimationEffects();
- Channel channel = mMainActivity.getCurrentChannel();
- if (!Objects.equals(mCurrentChannel, channel)) {
- mBlockingContentRating = null;
+ mUpdateOnTune = updateOnTune;
+ if (mUpdateOnTune) {
if (isShown()) {
scheduleHide();
}
- }
- mCurrentChannel = channel;
- mChannelView.setVisibility(VISIBLE);
- if (info != null) {
- // If the current channels between ChannelTuner and TvView are different,
- // the stream information should not be seen.
- updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info
- : null);
+ mBlockingContentRating = null;
+ mCurrentChannel = mMainActivity.getCurrentChannel();
+ mCurrentChannelLogoExists =
+ mCurrentChannel != null && mCurrentChannel.channelLogoExists();
+ updateStreamInfo(null);
updateChannelInfo();
}
updateProgramInfo(mMainActivity.getCurrentProgram());
+ mChannelView.setVisibility(VISIBLE);
+ mUpdateOnTune = false;
}
- private void updateStreamInfo(StreamInfo info) {
+ /**
+ * Update channel banner view with stream info.
+ *
+ * @param info A StreamInfo that includes stream information.
+ */
+ public void updateStreamInfo(StreamInfo info) {
// Update stream information in a channel.
if (mLockType != LOCK_CHANNEL_INFO && info != null) {
updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark
@@ -414,9 +413,6 @@ 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);
- }
}
}
@@ -467,7 +463,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
mChannelLogoImageView.setImageBitmap(null);
mChannelLogoImageView.setVisibility(View.GONE);
- if (mCurrentChannel != null) {
+ if (mCurrentChannel != null && mCurrentChannelLogoExists) {
mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO,
mChannelLogoImageViewWidth, mChannelLogoImageViewHeight,
createChannelLogoCallback(this, mCurrentChannel));
@@ -550,8 +546,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
if (mResizeAnimator == null) {
String description = mProgramDescriptionTextView.getText().toString();
- boolean needFadeAnimation = !description.equals(mProgramDescriptionText);
- updateBannerHeight(needFadeAnimation);
+ boolean programDescriptionNeedFadeAnimation =
+ !description.equals(mProgramDescriptionText) && !mUpdateOnTune;
+ updateBannerHeight(programDescriptionNeedFadeAnimation);
} else {
mProgramInfoUpdatePendingByResizing = true;
}
@@ -559,9 +556,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private void updateProgramInfo(Program program) {
if (mLockType == LOCK_CHANNEL_INFO) {
- program = sLockedChannelProgram;
+ program = mLockedChannelProgram;
} else if (program == null || !program.isValid() || TextUtils.isEmpty(program.getTitle())) {
- program = sNoProgram;
+ program = mNoProgram;
}
if (mLastUpdatedProgram == null
@@ -590,9 +587,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mProgramDescriptionText = program.getDescription();
}
String description = mProgramDescriptionTextView.getText().toString();
- boolean needFadeAnimation = isProgramChanged
- || !description.equals(mProgramDescriptionText);
- updateBannerHeight(needFadeAnimation);
+ boolean programDescriptionNeedFadeAnimation = (isProgramChanged
+ || !description.equals(mProgramDescriptionText)) && !mUpdateOnTune;
+ updateBannerHeight(programDescriptionNeedFadeAnimation);
} else {
mProgramInfoUpdatePendingByResizing = true;
}
@@ -603,7 +600,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
if (program == null) {
return;
}
- updateProgramTextView(program == sLockedChannelProgram, program.getTitle(),
+ updateProgramTextView(program == mLockedChannelProgram, program.getTitle(),
program.getEpisodeDisplayTitle(getContext()));
}
@@ -630,9 +627,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mProgramTextView.setText(text);
}
- int width = mProgramDescriptionTextViewWidth
- - ((mChannelLogoImageView.getVisibility() != View.VISIBLE)
- ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart);
+ int width = mProgramDescriptionTextViewWidth + (mCurrentChannelLogoExists ?
+ 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart);
ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams();
lp.width = width;
mProgramTextView.setLayoutParams(lp);
@@ -655,23 +651,27 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
private void updateProgramRatings(Program program) {
- if (mBlockingContentRating != null) {
+ if (mLockType == LOCK_CHANNEL_INFO) {
+ for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ mContentRatingsTextViews[i].setVisibility(View.GONE);
+ }
+ } else 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);
+ } else {
+ 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);
+ }
}
}
}
@@ -769,7 +769,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mLastUpdatedProgram = program;
}
- private void updateBannerHeight(boolean needFadeAnimation) {
+ private void updateBannerHeight(boolean needProgramDescriptionFadeAnimation) {
Assert.assertNull(mResizeAnimator);
// Need to measure the layout height with the new description text.
CharSequence oldDescription = mProgramDescriptionTextView.getText();
@@ -785,12 +785,13 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
layoutParams.height = targetHeight;
setLayoutParams(layoutParams);
}
- } else if (mCurrentHeight != targetHeight || needFadeAnimation) {
+ } else if (mCurrentHeight != targetHeight || needProgramDescriptionFadeAnimation) {
// Restore description text for fade in/out animation.
- if (needFadeAnimation) {
+ if (needProgramDescriptionFadeAnimation) {
mProgramDescriptionTextView.setText(oldDescription);
}
- mResizeAnimator = createResizeAnimator(targetHeight, needFadeAnimation);
+ mResizeAnimator =
+ createResizeAnimator(targetHeight, needProgramDescriptionFadeAnimation);
mResizeAnimator.start();
}
}
diff --git a/src/com/android/tv/ui/KeypadChannelSwitchView.java b/src/com/android/tv/ui/KeypadChannelSwitchView.java
index abc05bad..ac5d841d 100644
--- a/src/com/android/tv/ui/KeypadChannelSwitchView.java
+++ b/src/com/android/tv/ui/KeypadChannelSwitchView.java
@@ -39,7 +39,7 @@ import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
diff --git a/src/com/android/tv/ui/SelectInputView.java b/src/com/android/tv/ui/SelectInputView.java
index 5e25ae43..88872a0a 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -18,7 +18,6 @@ package com.android.tv.ui;
import android.content.Context;
import android.content.res.Resources;
-import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
@@ -37,14 +36,13 @@ import android.widget.TextView;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -58,7 +56,7 @@ public class SelectInputView extends VerticalGridView implements
private final TvInputManagerHelper mTvInputManagerHelper;
private final List<TvInputInfo> mInputList = new ArrayList<>();
- private final InputsComparator mComparator = new InputsComparator();
+ private final TvInputManagerHelper.InputComparator mComparator;
private final Tracker mTracker;
private final DurationTimer mViewDurationTimer = new DurationTimer();
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@@ -149,6 +147,7 @@ public class SelectInputView extends VerticalGridView implements
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mTracker = appSingletons.getTracker();
mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
+ mComparator = new TvInputManagerHelper.InputComparator(context, mTvInputManagerHelper);
Resources resources = context.getResources();
mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
@@ -385,72 +384,6 @@ public class SelectInputView extends VerticalGridView implements
}
}
- private class InputsComparator implements Comparator<TvInputInfo> {
- @Override
- public int compare(TvInputInfo lhs, TvInputInfo rhs) {
- if (lhs == null) {
- return (rhs == null) ? 0 : 1;
- }
- if (rhs == null) {
- return -1;
- }
-
- boolean enabledL = isInputEnabled(lhs);
- boolean enabledR = isInputEnabled(rhs);
- if (enabledL != enabledR) {
- return enabledL ? -1 : 1;
- }
-
- int priorityL = getPriority(lhs);
- int priorityR = getPriority(rhs);
- if (priorityL != priorityR) {
- return priorityR - priorityL;
- }
-
- String customLabelL = (String) lhs.loadCustomLabel(getContext());
- String customLabelR = (String) rhs.loadCustomLabel(getContext());
- if (!TextUtils.equals(customLabelL, customLabelR)) {
- customLabelL = customLabelL == null ? "" : customLabelL;
- customLabelR = customLabelR == null ? "" : customLabelR;
- return customLabelL.compareToIgnoreCase(customLabelR);
- }
-
- String labelL = (String) lhs.loadLabel(getContext());
- String labelR = (String) rhs.loadLabel(getContext());
- labelL = labelL == null ? "" : labelL;
- labelR = labelR == null ? "" : labelR;
- return labelL.compareToIgnoreCase(labelR);
- }
-
- private int getPriority(TvInputInfo info) {
- switch (info.getType()) {
- case TvInputInfo.TYPE_TUNER:
- return 9;
- case TvInputInfo.TYPE_HDMI:
- HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo();
- if (hdmiInfo != null && hdmiInfo.isCecDevice()) {
- return 8;
- }
- return 7;
- case TvInputInfo.TYPE_DVI:
- return 6;
- case TvInputInfo.TYPE_COMPONENT:
- return 5;
- case TvInputInfo.TYPE_SVIDEO:
- return 4;
- case TvInputInfo.TYPE_COMPOSITE:
- return 3;
- case TvInputInfo.TYPE_DISPLAY_PORT:
- return 2;
- case TvInputInfo.TYPE_VGA:
- return 1;
- case TvInputInfo.TYPE_SCART:
- default:
- return 0;
- }
- }
- }
-
/**
* A callback interface for the input selection.
*/
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index cbe459fb..8f4f40f5 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -19,9 +19,16 @@ package com.android.tv.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
-import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.ApplicationErrorReport;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import android.media.PlaybackParams;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
@@ -38,7 +45,6 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
@@ -56,14 +62,20 @@ import com.android.tv.InputSessionManager;
import com.android.tv.InputSessionManager.TvViewSession;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.data.Program;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.util.DurationTimer;
+import com.android.tv.util.Debug;
import com.android.tv.analytics.Tracker;
+import com.android.tv.common.BuildConfig;
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.parental.ContentRatingsManager;
import com.android.tv.recommendation.NotificationService;
+import com.android.tv.util.ImageLoader;
import com.android.tv.util.NetworkUtils;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
@@ -105,14 +117,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private static final int FADING_IN = 2;
private static final int FADING_OUT = 3;
- // It is too small to see the description text without PIP_BLOCK_SCREEN_SCALE_FACTOR.
- private static final float PIP_BLOCK_SCREEN_SCALE_FACTOR = 1.2f;
-
private AppLayerTvView mTvView;
private TvViewSession mTvViewSession;
private Channel mCurrentChannel;
private TvInputManagerHelper mInputManagerHelper;
private ContentRatingsManager mContentRatingsManager;
+ private ParentalControlSettings mParentalControlSettings;
+ private ProgramDataManager mProgramDataManager;
@Nullable
private WatchedHistoryManager mWatchedHistoryManager;
private boolean mStarted;
@@ -126,6 +137,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
private boolean mHasClosedCaption = false;
private boolean mVideoAvailable;
+ private boolean mAudioAvailable;
private boolean mScreenBlocked;
private OnScreenBlockingChangedListener mOnScreenBlockedListener;
private TvContentRating mBlockedContentRating;
@@ -136,10 +148,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private boolean mParentControlEnabled;
private int mFixedSurfaceWidth;
private int mFixedSurfaceHeight;
- private boolean mIsPip;
- private int mScreenHeight;
- private int mShrunkenTvViewHeight;
private final boolean mCanModifyParentalControls;
+ private boolean mIsUnderShrunken;
@TimeShiftState private int mTimeShiftState = TIME_SHIFT_STATE_NONE;
private TimeShiftListener mTimeShiftListener;
@@ -156,12 +166,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
// A View to hide screen when there's problem in video playback.
private final BlockScreenView mHideScreenView;
-
- // A View to block screen until onContentAllowed is received if parental control is on.
- private final View mBlockScreenForTuneView;
+ private final int mHideScreenImageColorFilter;
// A spinner view to show buffering status.
private final View mBufferingSpinnerView;
+ private TuningBlockView mTuningBlockView;
+
+ // A View to block screen until onContentAllowed is received if parental control is on.
+ private final View mBlockScreenForTuneView;
// A View for fade-in/out animation
private final View mDimScreenView;
@@ -286,14 +298,66 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onVideoAvailable(String inputId) {
+ if (DEBUG) Log.d(TAG, "onVideoAvailable: {inputId=" + inputId + "}");
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start up of Live TV ends," +
+ " TunableTvView.onVideoAvailable resets timer");
+ long startUpDurationTime = Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
+ Debug.removeTimer(Debug.TAG_START_UP_TIMER);
+ if (BuildConfig.ENG
+ && startUpDurationTime > Debug.TIME_START_UP_DURATION_THRESHOLD) {
+ showAlertDialogForLongStartUp();
+ }
unhideScreenByVideoAvailability();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
}
}
+ private void showAlertDialogForLongStartUp() {
+ new AlertDialog.Builder(getContext()).setTitle(
+ getContext().getString(R.string.settings_send_feedback))
+ .setMessage("Because the start up time of Live channels is too long," +
+ " please send feedback")
+ .setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Intent intent = new Intent(Intent.ACTION_APP_ERROR);
+ ApplicationErrorReport report = new ApplicationErrorReport();
+ report.packageName = report.processName = getContext()
+ .getApplicationContext().getPackageName();
+ report.time = System.currentTimeMillis();
+ report.type = ApplicationErrorReport.TYPE_CRASH;
+
+ // Add the crash info to add title of feedback automatically.
+ ApplicationErrorReport.CrashInfo crash = new
+ ApplicationErrorReport.CrashInfo();
+ crash.exceptionClassName =
+ "Live TV start up takes long time";
+ crash.exceptionMessage =
+ "The start up time of Live TV is too long";
+ report.crashInfo = crash;
+
+ intent.putExtra(Intent.EXTRA_BUG_REPORT, report);
+ getContext().startActivity(intent);
+ }
+ })
+ .setNegativeButton(android.R.string.no, null)
+ .show();
+ }
+
@Override
public void onVideoUnavailable(String inputId, int reason) {
+ if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
+ && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "TunableTvView.onVideoUnAvailable reason = (" + reason
+ + ") and removes timer");
+ Debug.removeTimer(Debug.TAG_START_UP_TIMER);
+ } else {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "TunableTvView.onVideoUnAvailable reason = (" + reason + ")");
+ }
hideScreenByVideoAvailability(inputId, reason);
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
@@ -311,7 +375,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onContentAllowed(String inputId) {
mBlockScreenForTuneView.setVisibility(View.GONE);
- unblockScreenByContentRating();
+ mBlockedContentRating = null;
+ checkBlockScreenAndMuteNeeded();
if (mOnTuneListener != null) {
mOnTuneListener.onContentAllowed();
}
@@ -319,7 +384,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onContentBlocked(String inputId, TvContentRating rating) {
- blockScreenByContentRating(rating);
+ mBlockedContentRating = rating;
+ checkBlockScreenAndMuteNeeded();
if (mOnTuneListener != null) {
mOnTuneListener.onContentBlocked();
}
@@ -327,6 +393,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onTimeShiftStatusChanged(String inputId, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onTimeShiftStatusChanged: {inputId=" + inputId + ", status=" + status +
+ "}");
+ }
boolean available = status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE;
setTimeShiftAvailable(available);
}
@@ -378,6 +448,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen);
mHideScreenView.setImageVisibility(false);
mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
+ mHideScreenImageColorFilter = getResources().getColor(
+ R.color.tvview_block_image_color_filter, null);
mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
mDimScreenView = findViewById(R.id.dim);
mDimScreenView.animate().setListener(new AnimatorListenerAdapter() {
@@ -397,27 +469,23 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
});
}
- public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight,
- int shrunkenTvViewHeight) {
+ public void initialize(AppLayerTvView tvView, TuningBlockView tuningBlockView,
+ ProgramDataManager programDataManager, TvInputManagerHelper tvInputManagerHelper) {
mTvView = tvView;
+ mTuningBlockView = tuningBlockView;
+ mProgramDataManager = programDataManager;
+ mInputManagerHelper = tvInputManagerHelper;
+ mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
+ mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings();
if (mInputSessionManager != null) {
mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback);
} else {
mTvView.setCallback(mCallback);
}
- mIsPip = isPip;
- mScreenHeight = screenHeight;
- mShrunkenTvViewHeight = shrunkenTvViewHeight;
- mTvView.setZOrderOnTop(isPip);
copyLayoutParamsToTvView();
}
- public void start(TvInputManagerHelper tvInputManagerHelper) {
- mInputManagerHelper = tvInputManagerHelper;
- mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
- if (mStarted) {
- return;
- }
+ public void start() {
mStarted = true;
}
@@ -497,6 +565,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mWatchedHistoryManager = watchedHistoryManager;
}
+ /**
+ * Sets if the TunableTvView is under shrunken.
+ */
+ public void setIsUnderShrunken(boolean isUnderShrunken) {
+ mIsUnderShrunken = isUnderShrunken;
+ }
+
public boolean isPlaying() {
return mStarted;
}
@@ -519,6 +594,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* if the state is disconnected or channelId doesn't exist, it returns false.
*/
public boolean tuneTo(Channel channel, Bundle params, OnTuneListener listener) {
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("TunableTvView.tuneTo");
if (!mStarted) {
throw new IllegalStateException("TvView isn't started");
}
@@ -560,6 +636,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mVideoDisplayAspectRatio = 0f;
mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
mHasClosedCaption = false;
+ mBlockedContentRating = null;
mTimeShiftCurrentPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
// To reduce the IPCs, unregister the callback here and register it when necessary.
mTvView.setTimeShiftPositionCallback(null);
@@ -571,12 +648,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
hideScreenByVideoAvailability(mInputInfo.getId(),
TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ updateBlockScreenUI(false);
if (mTvViewSession != null) {
mTvViewSession.tune(channel, params, listener);
} else {
mTvView.tune(mInputInfo.getId(), mCurrentChannel.getUri(), params);
}
- unblockScreenByContentRating();
if (channel.isPassthrough()) {
mBlockScreenForTuneView.setVisibility(View.GONE);
} else if (mParentControlEnabled) {
@@ -715,6 +792,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
@Override
+ public boolean isVideoOrAudioAvailable() {
+ return mVideoAvailable || mAudioAvailable;
+ }
+
+ @Override
public int getVideoUnavailableReason() {
return mVideoUnavailableReason;
}
@@ -747,7 +829,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
- * Returns if the screen is blocked by {@link #blockScreen()}.
+ * Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}.
*/
public boolean isScreenBlocked() {
return mScreenBlocked;
@@ -766,38 +848,18 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
- * Locks current TV screen and mutes.
+ * Blocks/unblocks current TV screen and mutes.
* There would be black screen with lock icon in order to show that
* screen block is intended and not an error.
* TODO: Accept parameter to show lock icon or not.
+ *
+ * @param blockOrUnblock {@code true} to block the screen, or {@code false} to unblock.
*/
- public void blockScreen() {
- mScreenBlocked = true;
+ public void blockOrUnblockScreen(boolean blockOrUnblock) {
+ mScreenBlocked = blockOrUnblock;
checkBlockScreenAndMuteNeeded();
if (mOnScreenBlockedListener != null) {
- mOnScreenBlockedListener.onScreenBlockingChanged(true);
- }
- }
-
- private void blockScreenByContentRating(TvContentRating rating) {
- mBlockedContentRating = rating;
- checkBlockScreenAndMuteNeeded();
- }
-
- @Override
- @SuppressLint("RtlHardcoded")
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- if (mIsPip) {
- int height = bottom - top;
- float scale;
- if (mBlockScreenType == BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW) {
- scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mShrunkenTvViewHeight;
- } else {
- scale = height * PIP_BLOCK_SCREEN_SCALE_FACTOR / mScreenHeight;
- }
- // TODO: need to get UX confirmation.
- mBlockScreenView.scaleContainerView(scale);
+ mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock);
}
}
@@ -819,17 +881,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
|| tvViewLp.gravity != lp.gravity
|| tvViewLp.height != lp.height
|| tvViewLp.width != lp.width) {
- if (lp.topMargin == tvViewLp.topMargin && lp.leftMargin == tvViewLp.leftMargin
- && !BuildCompat.isAtLeastN()) {
- // HACK: If top and left position aren't changed and SurfaceHolder.setFixedSize is
- // used, SurfaceView doesn't catch the width and height change. It causes a bug that
- // PIP size change isn't shown when PIP is located TOP|LEFT. So we adjust 1 px for
- // small size PIP as a workaround.
- // Note: This framework issue has been fixed from NYC.
- tvViewLp.leftMargin = lp.leftMargin + 1;
- } else {
- tvViewLp.leftMargin = lp.leftMargin;
- }
+ tvViewLp.leftMargin = lp.leftMargin;
tvViewLp.topMargin = lp.topMargin;
tvViewLp.bottomMargin = lp.bottomMargin;
tvViewLp.rightMargin = lp.rightMargin;
@@ -945,96 +997,109 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private void checkBlockScreenAndMuteNeeded() {
updateBlockScreenUI(false);
- if (mScreenBlocked || mBlockedContentRating != null) {
- mute();
- if (mIsPip) {
- // If we don't make mTvView invisible, some frames are leaked when a user changes
- // PIP layout in options.
- // Note: When video is unavailable, we keep the mTvView's visibility, because
- // TIS implementation may not send video available with no surface.
- mTvView.setVisibility(View.INVISIBLE);
- }
- } else {
- unmuteIfPossible();
- if (mIsPip) {
- mTvView.setVisibility(View.VISIBLE);
- }
- }
- }
-
- public void unblockScreen() {
- mScreenBlocked = false;
- checkBlockScreenAndMuteNeeded();
- if (mOnScreenBlockedListener != null) {
- mOnScreenBlockedListener.onScreenBlockingChanged(false);
- }
- }
-
- private void unblockScreenByContentRating() {
- mBlockedContentRating = null;
- checkBlockScreenAndMuteNeeded();
+ updateMuteStatus();
}
@UiThread
private void hideScreenByVideoAvailability(String inputId, int reason) {
mVideoAvailable = false;
+ mAudioAvailable = false;
mVideoUnavailableReason = reason;
if (mInternetCheckTask != null) {
mInternetCheckTask.cancel(true);
mInternetCheckTask = null;
}
+ if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
+ // Tuning block view will apply animation when unhide screen, so let's end the
+ // animation if it is running.
+ mTuningBlockView.endFadeOutAnimator();
+ }
switch (reason) {
case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(R.string.tvview_msg_audio_only);
+ mTuningBlockView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- unmuteIfPossible();
+ mAudioAvailable = true;
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
mBufferingSpinnerView.setVisibility(VISIBLE);
- mute();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setText(R.string.tvview_msg_weak_signal);
+ mTuningBlockView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- mute();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(null);
mBufferingSpinnerView.setVisibility(VISIBLE);
- mute();
+ if (shouldShowImageForTuning()) {
+ mHideScreenView.setVisibility(GONE);
+ mTuningBlockView.setVisibility(VISIBLE);
+ mTuningBlockView.setImageVisibility(false);
+ showImageForTuning();
+ } else {
+ mHideScreenView.setVisibility(VISIBLE);
+ mHideScreenView.setImageVisibility(false);
+ mHideScreenView.setText(null);
+ mTuningBlockView.setVisibility(GONE);
+ }
break;
case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(null);
+ mTuningBlockView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- mute();
break;
case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(getTuneConflictMessage(inputId));
+ mTuningBlockView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- mute();
break;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
mHideScreenView.setVisibility(VISIBLE);
mHideScreenView.setImageVisibility(false);
mHideScreenView.setText(null);
+ mTuningBlockView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- mute();
if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
mInternetCheckTask = new InternetCheckTask();
mInternetCheckTask.execute();
}
break;
}
+ updateMuteStatus();
+ }
+
+ private boolean shouldShowImageForTuning() {
+ if (getWidth() == 0 || getWidth() == 0 || mCurrentChannel == null || !isBundledInput()
+ || mIsUnderShrunken || (mParentControlEnabled && (mCurrentChannel.isLocked()))) {
+ return false;
+ }
+ Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
+ if (currentProgram == null) {
+ return false;
+ }
+ TvContentRating rating =
+ mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings());
+ return !(mParentControlEnabled && rating != null);
+ }
+
+ private void showImageForTuning() {
+ mTuningBlockView.setImage(null);
+ if (mCurrentChannel == null) {
+ return;
+ }
+ Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
+ if (currentProgram != null) {
+ currentProgram.loadPosterArt(getContext(), getWidth(), getHeight(),
+ createProgramPosterArtCallback(mTuningBlockView, mCurrentChannel.getId()));
+ }
}
private String getTuneConflictMessage(String inputId) {
@@ -1052,25 +1117,43 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private void unhideScreenByVideoAvailability() {
mVideoAvailable = true;
+ mAudioAvailable = true;
+ mTuningBlockView.hideWithAnimationIfNeeded();
mHideScreenView.setVisibility(GONE);
mBufferingSpinnerView.setVisibility(GONE);
- unmuteIfPossible();
- }
-
- private void unmuteIfPossible() {
- if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) {
- unmute();
+ updateMuteStatus();
+ }
+
+ private void updateMuteStatus() {
+ // Workaround: TunerTvInputService uses AC3 pass-through implementation, which disables
+ // audio tracks to enforce the mute request. We don't want to send mute request if we are
+ // not going to block the screen to prevent the video jankiness resulted by disabling audio
+ // track before the playback is started. In other way, we should send unmute request before
+ // the playback is started, because TunerTvInput will remember the muted state and mute
+ // itself right way when the playback is going to be started, which results the initial
+ // jankiness, too.
+ boolean isBundledInput = isBundledInput();
+ if ((isBundledInput || mAudioAvailable) && !mScreenBlocked
+ && mBlockedContentRating == null) {
+ if (mIsMuted) {
+ mIsMuted = false;
+ mTvView.setStreamVolume(mVolume);
+ }
+ } else {
+ if (!mIsMuted) {
+ if ((mInputInfo == null || isBundledInput)
+ && !mScreenBlocked && mBlockedContentRating == null) {
+ return;
+ }
+ mIsMuted = true;
+ mTvView.setStreamVolume(0);
+ }
}
}
- private void mute() {
- mIsMuted = true;
- mTvView.setStreamVolume(0);
- }
-
- private void unmute() {
- mIsMuted = false;
- mTvView.setStreamVolume(mVolume);
+ private boolean isBundledInput() {
+ return mInputInfo != null && mInputInfo.getType() == TvInputInfo.TYPE_TUNER
+ && Utils.isBundledInput(mInputInfo.getId());
}
/** Returns true if this view is faded out. */
@@ -1268,6 +1351,25 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
return mTimeShiftCurrentPositionMs;
}
+ private ImageLoader.ImageLoaderCallback<TuningBlockView> createProgramPosterArtCallback(
+ TuningBlockView view, final long channelId) {
+ return new ImageLoader.ImageLoaderCallback<TuningBlockView>(view) {
+ @Override
+ public void onBitmapLoaded(TuningBlockView view, @Nullable Bitmap posterArt) {
+ if (posterArt == null || getCurrentChannel() == null
+ || channelId != getCurrentChannel().getId()
+ || !shouldShowImageForTuning()) {
+ return;
+ }
+ Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt);
+ drawablePosterArt.mutate().setColorFilter(
+ mHideScreenImageColorFilter, PorterDuff.Mode.SRC_OVER);
+ view.setImage(drawablePosterArt);
+ view.setImageVisibility(true);
+ }
+ };
+ }
+
/**
* Used to receive the time-shift events.
*/
diff --git a/src/com/android/tv/ui/TuningBlockView.java b/src/com/android/tv/ui/TuningBlockView.java
new file mode 100644
index 00000000..2914b461
--- /dev/null
+++ b/src/com/android/tv/ui/TuningBlockView.java
@@ -0,0 +1,113 @@
+/*
+ * 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.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
+
+/**
+ * A view to block the screen while tuning channels.
+ */
+public class TuningBlockView extends FrameLayout{
+ private final static String TAG = "TuningBlockView";
+
+ private ImageView mImageView;
+ private Animator mFadeOut;
+
+ public TuningBlockView(Context context) {
+ this(context, null, 0);
+ }
+
+ public TuningBlockView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TuningBlockView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = (ImageView) findViewById(R.id.image);
+ mFadeOut = AnimatorInflater.loadAnimator(
+ getContext(), R.animator.tuning_block_view_fade_out);
+ mFadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setVisibility(GONE);
+ }
+ });
+ mFadeOut.setTarget(mImageView);
+ }
+
+ /**
+ * Sets image to the image view. This method should be called after finishing inflate the view.
+ */
+ public void setImage(Drawable imageDrawable) {
+ SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null");
+ mImageView.setImageDrawable(imageDrawable);
+ }
+
+ /**
+ * Sets the visibility of image view.
+ * This method should be called after finishing inflate the view.
+ */
+ public void setImageVisibility(boolean visible) {
+ SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null");
+ mImageView.setAlpha(1.0f);
+ mImageView.setVisibility(visible ? VISIBLE: GONE);
+ }
+
+ /**
+ * Returns if the image view is visible.
+ * This method should be called after finishing inflate the view.
+ */
+ public boolean isImageArtVisible() {
+ SoftPreconditions.checkState(mImageView != null, TAG, "imageView is null");
+ return mImageView.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * Hides the view with animation if needed.
+ */
+ public void hideWithAnimationIfNeeded() {
+ if (getVisibility() == VISIBLE && isImageArtVisible()) {
+ mFadeOut.start();
+ } else {
+ setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Ends the fade out animator.
+ */
+ public void endFadeOutAnimator() {
+ if (mFadeOut != null && mFadeOut.isRunning()) {
+ mFadeOut.end();
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index e14b286b..f86e6e95 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -39,6 +39,7 @@ import com.android.tv.MainActivity.KeyHandlerResultType;
import com.android.tv.R;
import com.android.tv.TimeShiftManager;
import com.android.tv.TvApplication;
+import com.android.tv.TvOptionsManager;
import com.android.tv.analytics.Tracker;
import com.android.tv.common.WeakHandler;
import com.android.tv.common.feature.CommonFeatures;
@@ -46,13 +47,14 @@ import com.android.tv.common.ui.setup.OnActionClickListener;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dialog.DvrHistoryDialogFragment;
import com.android.tv.dialog.FullscreenDialogFragment;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dialog.PinDialogFragment;
import com.android.tv.dialog.RecentlyWatchedDialogFragment;
import com.android.tv.dialog.SafeDismissDialogFragment;
import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.ui.DvrActivity;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
import com.android.tv.guide.ProgramGuide;
import com.android.tv.menu.Menu;
import com.android.tv.menu.Menu.MenuShowReason;
@@ -163,6 +165,7 @@ public class TvOverlayManager {
private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>();
static {
AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG);
@@ -195,10 +198,10 @@ public class TvOverlayManager {
private OnBackStackChangedListener mOnBackStackChangedListener;
public TvOverlayManager(MainActivity mainActivity, ChannelTuner channelTuner,
- TunableTvView tvView, KeypadChannelSwitchView keypadChannelSwitchView,
- ChannelBannerView channelBannerView, InputBannerView inputBannerView,
- SelectInputView selectInputView, ViewGroup sceneContainer,
- ProgramGuideSearchFragment searchFragment) {
+ TunableTvView tvView, TvOptionsManager optionsManager,
+ KeypadChannelSwitchView keypadChannelSwitchView, ChannelBannerView channelBannerView,
+ InputBannerView inputBannerView, SelectInputView selectInputView,
+ ViewGroup sceneContainer, ProgramGuideSearchFragment searchFragment) {
mMainActivity = mainActivity;
mChannelTuner = channelTuner;
ApplicationSingletons singletons = TvApplication.getSingletons(mainActivity);
@@ -225,7 +228,8 @@ public class TvOverlayManager {
});
// Menu
MenuView menuView = (MenuView) mainActivity.findViewById(R.id.menu);
- mMenu = new Menu(mainActivity, tvView, menuView, new MenuRowFactory(mainActivity, tvView),
+ mMenu = new Menu(mainActivity, tvView, optionsManager, menuView,
+ new MenuRowFactory(mainActivity, tvView),
new Menu.OnMenuVisibilityChangeListener() {
@Override
public void onMenuVisibilityChange(boolean visible) {
@@ -541,7 +545,7 @@ public class TvOverlayManager {
* Shows DVR manager.
*/
public void showDvrManager() {
- Intent intent = new Intent(mMainActivity, DvrActivity.class);
+ Intent intent = new Intent(mMainActivity, DvrBrowseActivity.class);
mMainActivity.startActivity(intent);
}
@@ -564,6 +568,14 @@ public class TvOverlayManager {
}
/**
+ * Shows DVR history dialog.
+ */
+ public void showDvrHistoryDialog() {
+ showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG,
+ new DvrHistoryDialogFragment(), false);
+ }
+
+ /**
* Shows banner view.
*/
public void showBanner() {
@@ -674,7 +686,7 @@ public class TvOverlayManager {
}
if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS) != 0) {
// Keeps side panels.
- } else if (mSideFragmentManager.isSidePanelVisible()) {
+ } else if (mSideFragmentManager.isActive()) {
if ((flags & FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY) != 0) {
mSideFragmentManager.hideSidePanel(withAnimation);
} else {
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index bf874fc7..8d3b14f7 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -52,9 +52,8 @@ import com.android.tv.data.DisplayMode;
import com.android.tv.util.TvSettings;
/**
- * The TvViewUiManager is responsible for handling UI layouting and animation of main and PIP
- * TvViews. It also control the settings regarding TvView UI such as display mode, PIP layout,
- * and PIP size.
+ * The TvViewUiManager is responsible for handling UI layouting and animation of main TvView.
+ * It also control the settings regarding TvView UI such as display mode.
*/
public class TvViewUiManager {
private static final String TAG = "TvViewManager";
@@ -69,18 +68,11 @@ public class TvViewUiManager {
private final Resources mResources;
private final FrameLayout mContentView;
private final TunableTvView mTvView;
- private final TunableTvView mPipView;
private final TvOptionsManager mTvOptionsManager;
- private final int mTvViewPapWidth;
private final int mTvViewShrunkenStartMargin;
private final int mTvViewShrunkenEndMargin;
- private final int mTvViewPapStartMargin;
- private final int mTvViewPapEndMargin;
private int mWindowWidth;
private int mWindowHeight;
- private final int mPipViewHorizontalMargin;
- private final int mPipViewTopMargin;
- private final int mPipViewBottomMargin;
private final SharedPreferences mSharedPreferences;
private final TimeInterpolator mLinearOutSlowIn;
private final TimeInterpolator mFastOutLinearIn;
@@ -113,9 +105,6 @@ public class TvViewUiManager {
private boolean mIsUnderShrunkenTvView;
private int mTvViewStartMargin;
private int mTvViewEndMargin;
- private int mPipLayout;
- private int mPipSize;
- private boolean mPipStarted;
private ObjectAnimator mTvViewAnimator;
private FrameLayout.LayoutParams mTvViewLayoutParams;
// TV view's position when the display mode is FULL. It is used to compute PIP location relative
@@ -130,12 +119,11 @@ public class TvViewUiManager {
private int mAppliedTvViewEndMargin;
private float mAppliedVideoDisplayAspectRatio;
- public TvViewUiManager(Context context, TunableTvView tvView, TunableTvView pipView,
+ public TvViewUiManager(Context context, TunableTvView tvView,
FrameLayout contentView, TvOptionsManager tvOptionManager) {
mContext = context;
mResources = mContext.getResources();
mTvView = tvView;
- mPipView = pipView;
mContentView = contentView;
mTvOptionsManager = tvOptionManager;
@@ -147,18 +135,12 @@ public class TvViewUiManager {
mWindowWidth = size.x;
mWindowHeight = size.y;
- // Have an assumption that PIP and TvView Shrinking happens only in full screen.
+ // Have an assumption that TvView Shrinking happens only in full screen.
mTvViewShrunkenStartMargin = mResources
.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_start);
mTvViewShrunkenEndMargin =
mResources.getDimensionPixelOffset(R.dimen.shrunken_tvview_margin_end)
+ mResources.getDimensionPixelSize(R.dimen.side_panel_width);
- int papMarginHorizontal = mResources
- .getDimensionPixelOffset(R.dimen.papview_margin_horizontal);
- int papSpacing = mResources.getDimensionPixelOffset(R.dimen.papview_spacing);
- mTvViewPapWidth = (mWindowWidth - papSpacing) / 2 - papMarginHorizontal;
- mTvViewPapStartMargin = papMarginHorizontal + mTvViewPapWidth + papSpacing;
- mTvViewPapEndMargin = papMarginHorizontal;
mTvViewFrame = createMarginLayoutParams(0, 0, 0, 0);
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
@@ -167,11 +149,6 @@ public class TvViewUiManager {
.loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
mFastOutLinearIn = AnimationUtils
.loadInterpolator(mContext, android.R.interpolator.fast_out_linear_in);
-
- mPipViewHorizontalMargin = mResources
- .getDimensionPixelOffset(R.dimen.pipview_margin_horizontal);
- mPipViewTopMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_top);
- mPipViewBottomMargin = mResources.getDimensionPixelOffset(R.dimen.pipview_margin_bottom);
}
public void onConfigurationChanged(final int windowWidth, final int windowHeight) {
@@ -200,18 +177,11 @@ public class TvViewUiManager {
*/
public void startShrunkenTvView() {
mIsUnderShrunkenTvView = true;
+ mTvView.setIsUnderShrunken(true);
mTvViewStartMarginBeforeShrunken = mTvViewStartMargin;
mTvViewEndMarginBeforeShrunken = mTvViewEndMargin;
- if (mPipStarted && getPipLayout() == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- float sidePanelWidth = mResources.getDimensionPixelOffset(R.dimen.side_panel_width);
- float factor = 1.0f - sidePanelWidth / mWindowWidth;
- int startMargin = (int) (mTvViewPapStartMargin * factor);
- int endMargin = (int) (mTvViewPapEndMargin * factor + sidePanelWidth);
- setTvViewMargin(startMargin, endMargin);
- } else {
- setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin);
- }
+ setTvViewMargin(mTvViewShrunkenStartMargin, mTvViewShrunkenEndMargin);
mDisplayModeBeforeShrunken = setDisplayMode(DisplayMode.MODE_NORMAL, false, true);
}
@@ -221,6 +191,7 @@ public class TvViewUiManager {
*/
public void endShrunkenTvView() {
mIsUnderShrunkenTvView = false;
+ mTvView.setIsUnderShrunken(false);
setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken);
setDisplayMode(mDisplayModeBeforeShrunken, false, true);
}
@@ -327,120 +298,6 @@ public class TvViewUiManager {
}
/**
- * Returns the current PIP layout. The layout should be one of
- * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
- * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
- * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
- */
- public int getPipLayout() {
- return mPipLayout;
- }
-
- /**
- * Sets the PIP layout. The layout should be one of
- * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
- * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
- * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
- *
- * @param storeInPreference if true, the stored value will be restored by
- * {@link #restorePipLayout()}.
- */
- public void setPipLayout(int pipLayout, boolean storeInPreference) {
- mPipLayout = pipLayout;
- if (storeInPreference) {
- TvSettings.setPipLayout(mContext, pipLayout);
- }
- updatePipView(mTvViewFrame);
- if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin);
- setDisplayMode(DisplayMode.MODE_NORMAL, false, false);
- } else {
- setTvViewMargin(0, 0);
- restoreDisplayMode(false);
- }
- mTvOptionsManager.onPipLayoutChanged(pipLayout);
- }
-
- /**
- * Restores the PIP layout which {@link #setPipLayout} lastly stores.
- */
- public void restorePipLayout() {
- setPipLayout(TvSettings.getPipLayout(mContext), false);
- }
-
- /**
- * Called when PIP is started.
- */
- public void onPipStart() {
- mPipStarted = true;
- updatePipView();
- mPipView.setVisibility(View.VISIBLE);
- }
-
- /**
- * Called when PIP is stopped.
- */
- public void onPipStop() {
- setTvViewMargin(0, 0);
- mPipView.setVisibility(View.GONE);
- mPipStarted = false;
- }
-
- /**
- * Called when PIP is resumed.
- */
- public void showPipForResume() {
- mPipView.setVisibility(View.VISIBLE);
- }
-
- /**
- * Called when PIP is paused.
- */
- public void hidePipForPause() {
- if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- mPipView.setVisibility(View.GONE);
- }
- }
-
- /**
- * Updates PIP view. It is usually called, when video resolution in PIP is updated.
- */
- public void updatePipView() {
- updatePipView(mTvViewFrame);
- }
-
- /**
- * Returns the size of the PIP view.
- */
- public int getPipSize() {
- return mPipSize;
- }
-
- /**
- * Sets PIP size and applies it immediately.
- *
- * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG}
- * and {@link TvSettings#PIP_SIZE_SMALL}.
- * @param storeInPreference if true, the stored value will be restored by
- * {@link #restorePipSize()}.
- */
- public void setPipSize(int pipSize, boolean storeInPreference) {
- mPipSize = pipSize;
- if (storeInPreference) {
- TvSettings.setPipSize(mContext, pipSize);
- }
- updatePipView(mTvViewFrame);
- mTvOptionsManager.onPipSizeChanged(pipSize);
- }
-
- /**
- * Restores the PIP size which {@link #setPipSize} lastly stores.
- */
- public void restorePipSize() {
- setPipSize(TvSettings.getPipSize(mContext), false);
- }
-
- /**
* This margins will be applied when applyDisplayMode is called.
*/
private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) {
@@ -540,113 +397,6 @@ public class TvViewUiManager {
} else {
mTvView.setLayoutParams(layoutParams);
}
- updatePipView(mTvViewFrame);
- }
- }
-
- /**
- * The redlines assume that the ratio of the TV screen is 16:9. If the radio is not 16:9, the
- * layout of PAP can be broken.
- */
- @SuppressLint("RtlHardcoded")
- private void updatePipView(MarginLayoutParams tvViewFrame) {
- if (!mPipStarted) {
- return;
- }
- int width;
- int height;
- int startMargin;
- int endMargin;
- int topMargin;
- int bottomMargin;
- int gravity;
-
- if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- gravity = Gravity.CENTER_VERTICAL | Gravity.START;
- height = tvViewFrame.height;
- float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
- if (videoDisplayAspectRatio <= 0f) {
- width = tvViewFrame.width;
- } else {
- width = (int) (height * videoDisplayAspectRatio);
- if (width > tvViewFrame.width) {
- width = tvViewFrame.width;
- }
- }
- startMargin = mResources.getDimensionPixelOffset(R.dimen.papview_margin_horizontal)
- * tvViewFrame.width / mTvViewPapWidth + (tvViewFrame.width - width) / 2;
- endMargin = 0;
- topMargin = 0;
- bottomMargin = 0;
- } else {
- int tvViewWidth = tvViewFrame.width;
- int tvViewHeight = tvViewFrame.height;
- int tvStartMargin = tvViewFrame.getMarginStart();
- int tvEndMargin = tvViewFrame.getMarginEnd();
- int tvTopMargin = tvViewFrame.topMargin;
- int tvBottomMargin = tvViewFrame.bottomMargin;
- float horizontalScaleFactor = (float) tvViewWidth / mWindowWidth;
- float verticalScaleFactor = (float) tvViewHeight / mWindowHeight;
-
- int maxWidth;
- if (mPipSize == TvSettings.PIP_SIZE_SMALL) {
- maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_width)
- * horizontalScaleFactor);
- height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_small_size_height)
- * verticalScaleFactor);
- } else if (mPipSize == TvSettings.PIP_SIZE_BIG) {
- maxWidth = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_width)
- * horizontalScaleFactor);
- height = (int) (mResources.getDimensionPixelSize(R.dimen.pipview_large_size_height)
- * verticalScaleFactor);
- } else {
- throw new IllegalArgumentException("Invalid PIP size: " + mPipSize);
- }
- float videoDisplayAspectRatio = mPipView.getVideoDisplayAspectRatio();
- if (videoDisplayAspectRatio <= 0f) {
- width = maxWidth;
- } else {
- width = (int) (height * videoDisplayAspectRatio);
- if (width > maxWidth) {
- width = maxWidth;
- }
- }
-
- startMargin = tvStartMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor);
- endMargin = tvEndMargin + (int) (mPipViewHorizontalMargin * horizontalScaleFactor);
- topMargin = tvTopMargin + (int) (mPipViewTopMargin * verticalScaleFactor);
- bottomMargin = tvBottomMargin + (int) (mPipViewBottomMargin * verticalScaleFactor);
-
- switch (mPipLayout) {
- case TvSettings.PIP_LAYOUT_TOP_LEFT:
- gravity = Gravity.TOP | Gravity.LEFT;
- break;
- case TvSettings.PIP_LAYOUT_TOP_RIGHT:
- gravity = Gravity.TOP | Gravity.RIGHT;
- break;
- case TvSettings.PIP_LAYOUT_BOTTOM_LEFT:
- gravity = Gravity.BOTTOM | Gravity.LEFT;
- break;
- case TvSettings.PIP_LAYOUT_BOTTOM_RIGHT:
- gravity = Gravity.BOTTOM | Gravity.RIGHT;
- break;
- default:
- throw new IllegalArgumentException("Invalid PIP location: " + mPipLayout);
- }
- }
-
- FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPipView.getLayoutParams();
- if (lp.width != width || lp.height != height || lp.getMarginStart() != startMargin
- || lp.getMarginEnd() != endMargin || lp.topMargin != topMargin
- || lp.bottomMargin != bottomMargin || lp.gravity != gravity) {
- lp.width = width;
- lp.height = height;
- lp.setMarginStart(startMargin);
- lp.setMarginEnd(endMargin);
- lp.topMargin = topMargin;
- lp.bottomMargin = bottomMargin;
- lp.gravity = gravity;
- mPipView.setLayoutParams(lp);
}
}
@@ -696,7 +446,6 @@ public class TvViewUiManager {
mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0);
interpolateMarginsRelative(mLastAnimatedTvViewFrame,
mOldTvViewFrame, mTvViewFrame, fraction);
- updatePipView(mLastAnimatedTvViewFrame);
}
});
}
diff --git a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
index d6ccdf6b..e5c23372 100644
--- a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
@@ -82,8 +82,9 @@ public class ClosedCaptionFragment extends SideFragment {
}
mItems.add(item);
- for (final TvTrackInfo track : tracks) {
- item = new ClosedCaptionOptionItem(getLabel(track),
+ for (int i = 0; i < tracks.size(); i++) {
+ final TvTrackInfo track = tracks.get(i);
+ item = new ClosedCaptionOptionItem(getLabel(track, i),
CaptionSettings.OPTION_ON, track.getId(), track.getLanguage());
if (isEnabled && track.getId().equals(trackId)) {
item.setChecked(true);
@@ -172,11 +173,11 @@ public class ClosedCaptionFragment extends SideFragment {
super.onDestroyView();
}
- private String getLabel(TvTrackInfo track) {
+ private String getLabel(TvTrackInfo track, int trackIndex) {
if (track.getLanguage() != null) {
return new Locale(track.getLanguage()).getDisplayName();
}
- return getString(R.string.default_language);
+ return getString(R.string.closed_caption_unknown_language, trackIndex + 1);
}
private class ClosedCaptionOptionItem extends RadioButtonItem {
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
index 0d189cca..fac24696 100644
--- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
@@ -18,8 +18,6 @@ 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 +25,7 @@ import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.BuildConfig;
+import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.experiments.Experiments;
import com.android.tv.tuner.TunerPreferences;
@@ -54,6 +53,14 @@ public class DeveloperOptionFragment extends SideFragment {
@Override
protected List<Item> getItemList() {
List<Item> items = new ArrayList<>();
+ if (CommonFeatures.DVR.isEnabled(getContext())) {
+ items.add(new ActionItem(getString(R.string.dev_item_dvr_history)) {
+ @Override
+ protected void onSelected() {
+ getMainActivity().getOverlayManager().showDvrHistoryDialog();
+ }
+ });
+ }
if (BuildConfig.ENG) {
items.add(new ActionItem(getString(R.string.dev_item_watch_history)) {
@Override
@@ -62,18 +69,6 @@ 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)) {
diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java
index 00f16427..4e47e75b 100644
--- a/src/com/android/tv/ui/sidepanel/Item.java
+++ b/src/com/android/tv/ui/sidepanel/Item.java
@@ -24,6 +24,7 @@ import android.view.ViewGroup;
public abstract class Item {
private View mItemView;
private boolean mEnabled = true;
+ private boolean mClickable = true;
public void setEnabled(boolean enabled) {
if (mEnabled != enabled) {
@@ -35,6 +36,16 @@ public abstract class Item {
}
/**
+ * Sets the item to be clickable or not.
+ */
+ public void setClickable(boolean clickable) {
+ mClickable = clickable;
+ if (mItemView != null) {
+ mItemView.setClickable(clickable);
+ }
+ }
+
+ /**
* Returns whether this item is enabled.
*/
public boolean isEnabled() {
@@ -64,6 +75,7 @@ public abstract class Item {
*/
protected void onUpdate() {
setEnabledInternal(mItemView, mEnabled);
+ mItemView.setClickable(mClickable);
}
protected abstract void onSelected();
diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
deleted file mode 100644
index dec017a8..00000000
--- a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
+++ /dev/null
@@ -1,170 +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.ui.sidepanel;
-
-import android.media.tv.TvInputInfo;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.tv.R;
-import com.android.tv.util.PipInputManager;
-import com.android.tv.util.PipInputManager.PipInput;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class PipInputSelectorFragment extends SideFragment {
- private static final String TAG = "PipInputSelector";
- private static final String TRACKER_LABEL = "PIP input source";
-
- private final List<Item> mInputItems = new ArrayList<>();
- private PipInputManager mPipInputManager;
- private PipInput mInitialPipInput;
- private boolean mSelected;
-
- private final PipInputManager.Listener mPipInputListener = new PipInputManager.Listener() {
- @Override
- public void onPipInputStateUpdated() {
- notifyDataSetChanged();
- }
-
- @Override
- public void onPipInputListUpdated() {
- refreshInputList();
- setItems(mInputItems);
- }
- };
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mPipInputManager = getMainActivity().getPipInputManager();
- mPipInputManager.addListener(mPipInputListener);
- getMainActivity().startShrunkenTvView(false, false);
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mInitialPipInput = mPipInputManager.getPipInput(getMainActivity().getPipChannel());
- if (mInitialPipInput == null) {
- Log.w(TAG, "PIP should be on");
- closeFragment();
- }
- int count = 0;
- for (Item item : mInputItems) {
- InputItem inputItem = (InputItem) item;
- if (Objects.equals(inputItem.mPipInput, mInitialPipInput)) {
- setSelectedPosition(count);
- break;
- }
- ++count;
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mPipInputManager.removeListener(mPipInputListener);
- if (!mSelected) {
- getMainActivity().tuneToChannelForPip(mInitialPipInput.getChannel());
- }
- getMainActivity().endShrunkenTvView();
- }
-
- @Override
- protected String getTitle() {
- return getString(R.string.side_panel_title_pip_input_source);
- }
-
- @Override
- public String getTrackerLabel() {
- return TRACKER_LABEL;
- }
-
- @Override
- protected List<Item> getItemList() {
- refreshInputList();
- return mInputItems;
- }
-
- private void refreshInputList() {
- mInputItems.clear();
- for (PipInput input : mPipInputManager.getPipInputList(false)) {
- mInputItems.add(new InputItem(input));
- }
- }
-
- private class InputItem extends RadioButtonItem {
- private final PipInput mPipInput;
-
- private InputItem(PipInput input) {
- super(input.getLongLabel());
- mPipInput = input;
- setEnabled(isAvailable());
- }
-
- @Override
- protected void onUpdate() {
- super.onUpdate();
- setEnabled(mPipInput.isAvailable());
- setChecked(mPipInput == mInitialPipInput);
- }
-
- @Override
- protected void onFocused() {
- super.onFocused();
- if (isEnabled()) {
- getMainActivity().tuneToChannelForPip(mPipInput.getChannel());
- }
- }
-
- @Override
- protected void onSelected() {
- super.onSelected();
- if (isEnabled()) {
- mSelected = true;
- closeFragment();
- }
- }
-
- private boolean isAvailable() {
- if (!mPipInput.isAvailable()) {
- return false;
- }
-
- // If this input shares the same parent with the current main input, you cannot select
- // it. (E.g. two HDMI CEC devices that are connected to HDMI port 1 through an A/V
- // receiver.)
- PipInput pipInput = mPipInputManager.getPipInput(getMainActivity().getCurrentChannel());
- if (pipInput == null) {
- return false;
- }
- TvInputInfo mainInputInfo = pipInput.getInputInfo();
- TvInputInfo pipInputInfo = mPipInput.getInputInfo();
- return mainInputInfo == null || pipInputInfo == null
- || !TextUtils.equals(mainInputInfo.getId(), pipInputInfo.getId())
- && !TextUtils.equals(mainInputInfo.getParentId(), pipInputInfo.getParentId());
- }
- }
-}
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index e8033a22..f6aa4f86 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,6 +16,8 @@
package com.android.tv.ui.sidepanel;
+import android.app.ApplicationErrorReport;
+import android.content.Intent;
import android.view.View;
import android.widget.Toast;
@@ -149,12 +151,26 @@ public class SettingsFragment extends SideFragment {
// But, we may be able to turn on channel lock feature regardless of the permission.
// It's TBD.
}
+ items.add(new ActionItem(getString(R.string.settings_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);
+ }
+ });
if (LicenseUtils.hasLicenses(activity.getAssets())) {
items.add(new LicenseActionItem(activity));
}
// Show version.
- items.add(new SimpleItem(getString(R.string.settings_menu_version),
- ((TvApplication) activity.getApplicationContext()).getVersionName()));
+ SimpleActionItem version = new SimpleActionItem(getString(R.string.settings_menu_version),
+ ((TvApplication) activity.getApplicationContext()).getVersionName());
+ version.setClickable(false);
+ items.add(version);
return items;
}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java
index 8df56cd2..bb815eb8 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -26,24 +26,26 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.HasTrackerLabel;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.util.SystemProperties;
+import com.android.tv.util.ViewCache;
import java.util.List;
public abstract class SideFragment extends Fragment implements HasTrackerLabel {
public static final int INVALID_POSITION = -1;
- private static final int RECYCLED_VIEW_POOL_SIZE = 7;
+ private static final int PRELOADED_VIEW_SIZE = 7;
private static final int[] PRELOADED_VIEW_IDS = {
R.layout.option_item_radio_button,
R.layout.option_item_channel_lock,
@@ -51,7 +53,8 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
R.layout.option_item_channel_check
};
- private static RecyclerView.RecycledViewPool sRecycledViewPool;
+ private static RecyclerView.RecycledViewPool sRecycledViewPool =
+ new RecyclerView.RecycledViewPool();
private VerticalGridView mListView;
private ItemAdapter mAdapter;
@@ -89,13 +92,6 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- if (sRecycledViewPool == null) {
- // sRecycledViewPool should be initialized by calling preloadRecycledViews()
- // before the entering animation of this fragment starts,
- // because it takes long time and if it is called after the animation starts (e.g. here)
- // it can affect the animation.
- throw new IllegalStateException("The RecyclerView pool has not been initialized.");
- }
View view = inflater.inflate(getFragmentLayoutResourceId(), container, false);
TextView textView = (TextView) view.findViewById(R.id.side_panel_title);
@@ -236,30 +232,27 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
}
/**
- * Preloads the view holders.
+ * Preloads the item views.
*/
- public static void preloadRecycledViews(Context context) {
- if (sRecycledViewPool != null) {
- return;
- }
- sRecycledViewPool = new RecyclerView.RecycledViewPool();
+ public static void preloadItemViews(Context context) {
LayoutInflater inflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ // Use a fake parent to make the layoutParams set correctly.
+ ViewGroup fakeParent = new LinearLayout(context);
for (int id : PRELOADED_VIEW_IDS) {
- sRecycledViewPool.setMaxRecycledViews(id, RECYCLED_VIEW_POOL_SIZE);
- for (int j = 0; j < RECYCLED_VIEW_POOL_SIZE; ++j) {
- ItemAdapter.ViewHolder viewHolder = new ItemAdapter.ViewHolder(
- inflater.inflate(id, null, false));
- sRecycledViewPool.putRecycledView(viewHolder);
+ sRecycledViewPool.setMaxRecycledViews(id, PRELOADED_VIEW_SIZE);
+ for (int j = 0; j < PRELOADED_VIEW_SIZE; ++j) {
+ View view = inflater.inflate(id, fakeParent, false);
+ ViewCache.getInstance().putView(id, view);
}
}
}
/**
- * Releases the pre-loaded view holders.
+ * Releases the recycled view pool.
*/
- public static void releasePreloadedRecycledViews() {
- sRecycledViewPool = null;
+ public static void releaseRecycledViewPool() {
+ sRecycledViewPool.clear();
}
private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {
@@ -278,7 +271,11 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false));
+ View view = ViewCache.getInstance().getView(viewType);
+ if (view == null) {
+ view = mLayoutInflater.inflate(viewType, parent, false);
+ }
+ return new ViewHolder(view);
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
index 553cd9d7..4398b3f3 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
@@ -99,7 +99,6 @@ public class SideFragmentManager {
* Shows the given {@link SideFragment}.
*/
public void show(SideFragment sideFragment, boolean showEnterAnimation) {
- SideFragment.preloadRecycledViews(mActivity);
if (isHiding()) {
mHideAnimator.end();
}
@@ -178,7 +177,6 @@ public class SideFragmentManager {
* @param withAnimation specifies if animation should be shown.
*/
public void showSidePanel(boolean withAnimation) {
- SideFragment.preloadRecycledViews(mActivity);
if (mFragmentCount == 0) {
return;
}
diff --git a/src/com/android/tv/ui/sidepanel/SimpleItem.java b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java
index 52a5f13f..42553b66 100644
--- a/src/com/android/tv/ui/sidepanel/SimpleItem.java
+++ b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java
@@ -19,12 +19,12 @@ package com.android.tv.ui.sidepanel;
/**
* A simple item which shows title and description.
*/
-public class SimpleItem extends ActionItem {
- public SimpleItem(String title) {
+public class SimpleActionItem extends ActionItem {
+ public SimpleActionItem(String title) {
super(title);
}
- public SimpleItem(String title, String description) {
+ public SimpleActionItem(String title, String description) {
super(title, description);
}
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 78243642..3cc91e40 100644
--- a/src/com/android/tv/util/AsyncDbTask.java
+++ b/src/com/android/tv/util/AsyncDbTask.java
@@ -31,7 +31,7 @@ import android.util.Range;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java
new file mode 100644
index 00000000..67a2683d
--- /dev/null
+++ b/src/com/android/tv/util/Debug.java
@@ -0,0 +1,60 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class only for help developers.
+ */
+public class Debug {
+ /**
+ * A threshold of start up time, when the start up time of Live TV is more than it,
+ * a warning will show to the developer.
+ */
+ public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6);
+ /**
+ * Tag for measuring start up time of Live TV.
+ */
+ public static final String TAG_START_UP_TIMER = "start_up_timer";
+
+ /**
+ * A global map for duration timers.
+ */
+ private final static Map<String, DurationTimer> sTimerMap = new HashMap<>();
+
+ /**
+ * Returns the global duration timer by tag.
+ */
+ public static DurationTimer getTimer(String tag) {
+ if (sTimerMap.get(tag) != null) {
+ return sTimerMap.get(tag);
+ }
+ DurationTimer timer = new DurationTimer(tag, true);
+ sTimerMap.put(tag, timer);
+ return timer;
+ }
+
+ /**
+ * Removes the global duration timer by tag.
+ */
+ public static DurationTimer removeTimer(String tag) {
+ return sTimerMap.remove(tag);
+ }
+}
diff --git a/src/com/android/tv/analytics/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java
index ad2d91f8..b6221496 100644
--- a/src/com/android/tv/analytics/DurationTimer.java
+++ b/src/com/android/tv/util/DurationTimer.java
@@ -14,17 +14,30 @@
* limitations under the License.
*/
-package com.android.tv.analytics;
+package com.android.tv.util;
import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.tv.common.BuildConfig;
/**
* Times a duration.
*/
public final class DurationTimer {
+ private static final String TAG = "DurationTimer";
public static final long TIME_NOT_SET = -1;
private long startTimeMs = TIME_NOT_SET;
+ private String mTag = TAG;
+ private boolean mLogEngOnly;
+
+ public DurationTimer() { }
+
+ public DurationTimer(String tag, boolean logEngOnly) {
+ mTag = tag;
+ mLogEngOnly = logEngOnly;
+ }
/**
* Returns true if the timer is running.
@@ -59,4 +72,13 @@ public final class DurationTimer {
startTimeMs = TIME_NOT_SET;
return duration;
}
+
+ /**
+ * Adds information and duration time to the log.
+ */
+ public void log(String message) {
+ if (isRunning() && (!mLogEngOnly || BuildConfig.ENG)) {
+ Log.i(mTag, message + " : " + getDuration() + "ms");
+ }
+ }
}
diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java
index 8e3b59e9..2ae6db18 100644
--- a/src/com/android/tv/util/LocationUtils.java
+++ b/src/com/android/tv/util/LocationUtils.java
@@ -16,7 +16,9 @@
package com.android.tv.util;
+import android.Manifest;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
@@ -25,6 +27,7 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
+import com.android.tv.tuner.util.PostalCodeUtils;
import java.io.IOException;
import java.util.List;
@@ -39,6 +42,7 @@ public class LocationUtils {
private static Context sApplicationContext;
private static Address sAddress;
+ private static String sCountry;
private static IOException sError;
/**
@@ -59,6 +63,19 @@ public class LocationUtils {
return null;
}
+ /**
+ * Returns the current country.
+ */
+ public static synchronized String getCurrentCountry(Context context) {
+ if (sCountry != null) {
+ return sCountry;
+ }
+ if (sCountry == null) {
+ sCountry = context.getResources().getConfiguration().locale.getCountry();
+ }
+ return sCountry;
+ }
+
private static void updateAddress(Location location) {
if (DEBUG) Log.d(TAG, "Updating address with " + location);
if (location == null) {
@@ -68,9 +85,14 @@ public class LocationUtils {
try {
List<Address> addresses = geocoder.getFromLocation(
location.getLatitude(), location.getLongitude(), 1);
- if (addresses != null) {
+ if (addresses != null && !addresses.isEmpty()) {
sAddress = addresses.get(0);
if (DEBUG) Log.d(TAG, "Got " + sAddress);
+ try {
+ PostalCodeUtils.updatePostalCode(sApplicationContext);
+ } catch (Exception e) {
+ // Do nothing
+ }
} else {
if (DEBUG) Log.d(TAG, "No address returned");
}
diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java
new file mode 100644
index 00000000..e3688392
--- /dev/null
+++ b/src/com/android/tv/util/Partner.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.media.tv.TvInputInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This file refers to Partner.java in LeanbackLauncher. Interact with partner customizations. There
+ * can only be one set of customizations on a device, and it must be bundled with the system.
+ */
+public class Partner {
+ private static final String TAG = "Partner";
+ /** Marker action used to discover partner */
+ private static final String ACTION_PARTNER_CUSTOMIZATION =
+ "com.google.android.leanbacklauncher.action.PARTNER_CUSTOMIZATION";
+
+ /** ID tags for device input types */
+ public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners";
+ public static final String INPUT_TYPE_TUNER = "input_type_tuner";
+ public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical";
+ public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder";
+ public static final String INPUT_TYPE_CEC_PLAYBACK = "input_type_cec_playback";
+ public static final String INPUT_TYPE_MHL_MOBILE = "input_type_mhl_mobile";
+ public static final String INPUT_TYPE_HDMI = "input_type_hdmi";
+ public static final String INPUT_TYPE_DVI = "input_type_dvi";
+ public static final String INPUT_TYPE_COMPONENT = "input_type_component";
+ public static final String INPUT_TYPE_SVIDEO = "input_type_svideo";
+ public static final String INPUT_TYPE_COMPOSITE = "input_type_composite";
+ public static final String INPUT_TYPE_DISPLAY_PORT = "input_type_displayport";
+ public static final String INPUT_TYPE_VGA = "input_type_vga";
+ public static final String INPUT_TYPE_SCART = "input_type_scart";
+ public static final String INPUT_TYPE_OTHER = "input_type_other";
+
+ private static final String INPUTS_ORDER = "home_screen_inputs_ordering";
+ private static final String TYPE_ARRAY = "array";
+
+ private static Partner sPartner;
+ private static final Object sLock = new Object();
+
+ private final String mPackageName;
+ private final String mReceiverName;
+ private final Resources mResources;
+
+ private static final Map<String, Integer> INPUT_TYPE_MAP = new HashMap<>();
+ static {
+ INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_LOGICAL, TvInputManagerHelper.TYPE_CEC_DEVICE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_RECORDER, TvInputManagerHelper.TYPE_CEC_DEVICE_RECORDER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_PLAYBACK, TvInputManagerHelper.TYPE_CEC_DEVICE_PLAYBACK);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_MHL_MOBILE, TvInputManagerHelper.TYPE_MHL_MOBILE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_HDMI, TvInputInfo.TYPE_HDMI);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_DVI, TvInputInfo.TYPE_DVI);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_COMPONENT, TvInputInfo.TYPE_COMPONENT);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_SVIDEO, TvInputInfo.TYPE_SVIDEO);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_COMPOSITE, TvInputInfo.TYPE_COMPOSITE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_DISPLAY_PORT, TvInputInfo.TYPE_DISPLAY_PORT);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_VGA, TvInputInfo.TYPE_VGA);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_SCART, TvInputInfo.TYPE_SCART);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_OTHER, TvInputInfo.TYPE_OTHER);
+ }
+
+ private Partner(String packageName, String receiverName, Resources res) {
+ mPackageName = packageName;
+ mReceiverName = receiverName;
+ mResources = res;
+ }
+
+ /** Returns partner instance. */
+ public static Partner getInstance(Context context) {
+ PackageManager pm = context.getPackageManager();
+ synchronized (sLock) {
+ ResolveInfo info = getPartnerResolveInfo(pm);
+ if (info != null) {
+ final String packageName = info.activityInfo.packageName;
+ final String receiverName = info.activityInfo.name;
+ try {
+ final Resources res = pm.getResourcesForApplication(packageName);
+ sPartner = new Partner(packageName, receiverName, res);
+ sPartner.sendInitBroadcast(context);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to find resources for " + packageName);
+ }
+ }
+ if (sPartner == null) {
+ sPartner = new Partner(null, null, null);
+ }
+ }
+ return sPartner;
+ }
+
+ /** Resets the Partner instance to handle the partner package has changed. */
+ public static void reset(Context context, String packageName) {
+ synchronized (sLock) {
+ if (sPartner != null && !TextUtils.isEmpty(packageName)) {
+ if (packageName.equals(sPartner.mPackageName)) {
+ // Force a refresh, so we send an Init to the updated package
+ sPartner = null;
+ getInstance(context);
+ }
+ }
+ }
+ }
+
+ /** This method is used to send init broadcast to the new/changed partner package. */
+ private void sendInitBroadcast(Context context) {
+ if (!TextUtils.isEmpty(mPackageName) && !TextUtils.isEmpty(mReceiverName)) {
+ Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION);
+ final ComponentName componentName = new ComponentName(mPackageName, mReceiverName);
+ intent.setComponent(componentName);
+ intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ context.sendBroadcast(intent);
+ }
+ }
+
+ /** Returns the order of inputs. */
+ public Map<Integer, Integer> getInputsOrderMap() {
+ HashMap<Integer, Integer> map = new HashMap<>();
+ if (mResources != null && !TextUtils.isEmpty(mPackageName)) {
+ String[] inputsArray = null;
+ final int resId = mResources.getIdentifier(INPUTS_ORDER, TYPE_ARRAY, mPackageName);
+ if (resId != 0) {
+ inputsArray = mResources.getStringArray(resId);
+ }
+ if (inputsArray != null) {
+ int priority = 0;
+ for (String input : inputsArray) {
+ Integer type = INPUT_TYPE_MAP.get(input);
+ if (type != null) {
+ map.put(type, priority++);
+ }
+ }
+ }
+ }
+ return map;
+ }
+
+ private static ResolveInfo getPartnerResolveInfo(PackageManager pm) {
+ final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION);
+ ResolveInfo partnerInfo = null;
+ for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
+ if (isSystemApp(info)) {
+ partnerInfo = info;
+ break;
+ }
+ }
+ return partnerInfo;
+ }
+
+ protected static boolean isSystemApp(ResolveInfo info) {
+ return (info.activityInfo != null
+ && info.activityInfo.applicationInfo != null
+ && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
+ }
+}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
deleted file mode 100644
index 2c51d5a0..00000000
--- a/src/com/android/tv/util/PipInputManager.java
+++ /dev/null
@@ -1,432 +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.util;
-
-import android.content.Context;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
-import android.media.tv.TvInputManager.TvInputCallback;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.tv.ChannelTuner;
-import com.android.tv.R;
-import com.android.tv.data.Channel;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP.
- * Hidden inputs should not be visible to the users.
- */
-public class PipInputManager {
- private static final String TAG = "PipInputManager";
-
- // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input.
- // Therefore, we define a fake input id for the unified input.
- private static final String TUNER_INPUT_ID = "tuner_input_id";
-
- private final Context mContext;
- private final TvInputManagerHelper mInputManager;
- private final ChannelTuner mChannelTuner;
- private boolean mStarted;
- private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput
- private final Set<Listener> mListeners = new ArraySet<>();
-
- private final TvInputCallback mTvInputCallback = new TvInputCallback() {
- @Override
- public void onInputAdded(String inputId) {
- TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- if (input.isPassthroughInput()) {
- boolean available = mInputManager.getInputState(input)
- == TvInputManager.INPUT_STATE_CONNECTED;
- mPipInputMap.put(inputId, new PipInput(inputId, available));
- } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) {
- boolean available = mChannelTuner.getBrowsableChannelCount() != 0;
- mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available));
- } else {
- return;
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- PipInput pipInput = mPipInputMap.remove(inputId);
- if (pipInput == null) {
- if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) {
- Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager");
- return;
- }
- if (mInputManager.getTunerTvInputSize() > 0) {
- return;
- }
- mPipInputMap.remove(TUNER_INPUT_ID);
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- @Override
- public void onInputStateChanged(String inputId, int state) {
- PipInput pipInput = mPipInputMap.get(inputId);
- if (pipInput == null) {
- // For tuner input, state change is handled in mChannelTunerListener.
- return;
- }
- pipInput.updateAvailability();
- }
- };
-
- private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
- @Override
- public void onLoadFinished() { }
-
- @Override
- public void onCurrentChannelUnavailable(Channel channel) { }
-
- @Override
- public void onBrowsableChannelListChanged() {
- PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID);
- if (tunerInput == null) {
- return;
- }
- tunerInput.updateAvailability();
- }
-
- @Override
- public void onChannelChanged(Channel previousChannel, Channel currentChannel) {
- if (previousChannel != null && currentChannel != null
- && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) {
- // Channel change between channels for tuner inputs.
- return;
- }
- PipInput previousMainInput = getPipInput(previousChannel);
- if (previousMainInput != null) {
- previousMainInput.updateAvailability();
- }
- PipInput currentMainInput = getPipInput(currentChannel);
- if (currentMainInput != null) {
- currentMainInput.updateAvailability();
- }
- }
- };
-
- public PipInputManager(Context context, TvInputManagerHelper inputManager,
- ChannelTuner channelTuner) {
- mContext = context;
- mInputManager = inputManager;
- mChannelTuner = channelTuner;
- }
-
- /**
- * Starts {@link PipInputManager}.
- */
- public void start() {
- if (mStarted) {
- return;
- }
- mStarted = true;
- mInputManager.addCallback(mTvInputCallback);
- mChannelTuner.addListener(mChannelTunerListener);
- initializePipInputList();
- }
-
- /**
- * Stops {@link PipInputManager}.
- */
- public void stop() {
- if (!mStarted) {
- return;
- }
- mStarted = false;
- mInputManager.removeCallback(mTvInputCallback);
- mChannelTuner.removeListener(mChannelTunerListener);
- mPipInputMap.clear();
- }
-
- /**
- * Adds a {@link PipInputManager.Listener}.
- */
- public void addListener(Listener listener) {
- mListeners.add(listener);
- }
-
- /**
- * Removes a {@link PipInputManager.Listener}.
- */
- public void removeListener(Listener listener) {
- mListeners.remove(listener);
- }
-
- /**
- * Gets the size of inputs for PIP.
- *
- * <p>The hidden inputs are not counted.
- *
- * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link
- * PipInput#isAvailable()} for the details of availability.
- */
- public int getPipInputSize(boolean availableOnly) {
- int count = 0;
- for (PipInput pipInput : mPipInputMap.values()) {
- if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
- ++count;
- }
- if (pipInput.isPassthrough()) {
- TvInputInfo info = pipInput.getInputInfo();
- // Do not count HDMI ports if a CEC device is directly connected to the port.
- if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
- --count;
- }
- }
- }
- return count;
- }
-
- /**
- * Gets the list of inputs for PIP..
- *
- * <p>The hidden inputs are excluded.
- *
- * @param availableOnly If true, it returns only available PIP inputs. Please see {@link
- * PipInput#isAvailable()} for the details of availability.
- */
- public List<PipInput> getPipInputList(boolean availableOnly) {
- List<PipInput> pipInputs = new ArrayList<>();
- List<PipInput> removeInputs = new ArrayList<>();
- for (PipInput pipInput : mPipInputMap.values()) {
- if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
- pipInputs.add(pipInput);
- }
- if (pipInput.isPassthrough()) {
- TvInputInfo info = pipInput.getInputInfo();
- // Do not show HDMI ports if a CEC device is directly connected to the port.
- if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
- removeInputs.add(mPipInputMap.get(info.getParentId()));
- }
- }
- }
- if (!removeInputs.isEmpty()) {
- pipInputs.removeAll(removeInputs);
- }
- Collections.sort(pipInputs, new Comparator<PipInput>() {
- @Override
- public int compare(PipInput lhs, PipInput rhs) {
- if (!lhs.mIsPassthrough) {
- return -1;
- }
- if (!rhs.mIsPassthrough) {
- return 1;
- }
- String a = lhs.getLabel();
- String b = rhs.getLabel();
- return a.compareTo(b);
- }
- });
- return pipInputs;
- }
-
- /**
- * Returns an PIP input corresponding to {@code channel}.
- */
- public PipInput getPipInput(Channel channel) {
- if (channel == null) {
- return null;
- }
- if (channel.isPassthrough()) {
- return mPipInputMap.get(channel.getInputId());
- } else {
- return mPipInputMap.get(TUNER_INPUT_ID);
- }
- }
-
- /**
- * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example,
- * two channels from different tuner inputs are also in the same input "Tuner" from PIP
- * point of view.
- */
- public boolean areInSamePipInput(Channel channel1, Channel channel2) {
- PipInput input1 = getPipInput(channel1);
- PipInput input2 = getPipInput(channel2);
- return input1 != null && input2 != null
- && getPipInput(channel1).equals(getPipInput(channel2));
- }
-
- private void initializePipInputList() {
- boolean hasTunerInput = false;
- for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) {
- if (input.isPassthroughInput()) {
- boolean available = mInputManager.getInputState(input)
- == TvInputManager.INPUT_STATE_CONNECTED;
- mPipInputMap.put(input.getId(), new PipInput(input.getId(), available));
- } else if (!hasTunerInput) {
- hasTunerInput = true;
- boolean available = mChannelTuner.getBrowsableChannelCount() != 0;
- mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available));
- }
- }
- PipInput input = getPipInput(mChannelTuner.getCurrentChannel());
- if (input != null) {
- input.updateAvailability();
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- /**
- * Listeners to notify PIP input state changes.
- */
- public interface Listener {
- /**
- * Called when the state (availability) of PIP inputs is changed.
- */
- void onPipInputStateUpdated();
-
- /**
- * Called when the list of PIP inputs is changed.
- */
- void onPipInputListUpdated();
- }
-
- /**
- * Input class for PIP. It has useful methods for PIP handling.
- */
- public class PipInput {
- private final String mInputId;
- private final boolean mIsPassthrough;
- private final TvInputInfo mInputInfo;
- private boolean mAvailable;
-
- private PipInput(String inputId, boolean available) {
- mInputId = inputId;
- mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID);
- if (mIsPassthrough) {
- mInputInfo = mInputManager.getTvInputInfo(mInputId);
- } else {
- mInputInfo = null;
- }
- mAvailable = available;
- }
-
- /**
- * Returns the {@link TvInputInfo} object that matches to this PIP input.
- */
- public TvInputInfo getInputInfo() {
- return mInputInfo;
- }
-
- /**
- * Returns {@code true}, if the input is available for PIP. If a channel of an input is
- * already played or an input is not connected state or there is no browsable channel, the
- * input is unavailable.
- */
- public boolean isAvailable() {
- return mAvailable;
- }
-
- /**
- * Returns true, if the input is a passthrough TV input.
- */
- public boolean isPassthrough() {
- return mIsPassthrough;
- }
-
- /**
- * Gets a channel to play in a PIP view.
- */
- public Channel getChannel() {
- if (mIsPassthrough) {
- return Channel.createPassthroughChannel(mInputId);
- } else {
- return mChannelTuner.findNearestBrowsableChannel(
- Utils.getLastWatchedChannelId(mContext));
- }
- }
-
- /**
- * Gets a label of the input.
- */
- public String getLabel() {
- if (mIsPassthrough) {
- return mInputInfo.loadLabel(mContext).toString();
- } else {
- return mContext.getString(R.string.input_selector_tuner_label);
- }
- }
-
- /**
- * Gets a long label including a customized label.
- */
- public String getLongLabel() {
- if (mIsPassthrough) {
- String customizedLabel = Utils.loadLabel(mContext, mInputInfo);
- String label = getLabel();
- if (label.equals(customizedLabel)) {
- return customizedLabel;
- }
- return customizedLabel + " (" + label + ")";
- } else {
- return mContext.getString(R.string.input_long_label_for_tuner);
- }
- }
-
- /**
- * Updates availability. It returns true, if availability is changed.
- */
- private void updateAvailability() {
- boolean available;
- // current playing input cannot be available for PIP.
- Channel currentChannel = mChannelTuner.getCurrentChannel();
- if (mIsPassthrough) {
- if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) {
- available = false;
- } else {
- available = mInputManager.getInputState(mInputId)
- == TvInputManager.INPUT_STATE_CONNECTED;
- }
- } else {
- if (currentChannel != null && !currentChannel.isPassthrough()) {
- available = false;
- } else {
- available = mChannelTuner.getBrowsableChannelCount() > 0;
- }
- }
- if (mAvailable != available) {
- mAvailable = available;
- for (Listener l : mListeners) {
- l.onPipInputStateUpdated();
- }
- }
- }
-
- private boolean isHidden() {
- // mInputInfo is null for the tuner input and it's always visible.
- return mInputInfo != null && mInputInfo.isHidden(mContext);
- }
- }
-}
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 4135bd4e..324afe73 100644
--- a/src/com/android/tv/util/RecurringRunner.java
+++ b/src/com/android/tv/util/RecurringRunner.java
@@ -57,12 +57,15 @@ public final class RecurringRunner {
mHandler = new Handler(mContext.getMainLooper());
}
- public void start() {
+ public void start(boolean restNextRunTime) {
SoftPreconditions.checkState(!mRunning, TAG, mName + " start is called twice.");
if (mRunning) {
return;
}
mRunning = true;
+ if (restNextRunTime) {
+ resetNextRunTime();
+ }
new AsyncTask<Void, Void, Long>() {
@Override
protected Long doInBackground(Void... params) {
@@ -76,6 +79,10 @@ public final class RecurringRunner {
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
+ public void start() {
+ start(false);
+ }
+
public void stop() {
mRunning = false;
mHandler.removeCallbacksAndMessages(null);
diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java
deleted file mode 100644
index b6e34d7a..00000000
--- a/src/com/android/tv/util/SearchManagerHelper.java
+++ /dev/null
@@ -1,61 +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.util;
-
-import android.app.SearchManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import java.lang.reflect.InvocationTargetException;
-
-/**
- * A convenience class for calling methods in android.app.SearchManager.
- */
-public final class SearchManagerHelper {
- private static final String TAG = "SearchManagerHelper";
-
- private static final Object sLock = new Object();
- private static SearchManagerHelper sInstance;
-
- private final SearchManager mSearchManager;
-
- private SearchManagerHelper(Context context) {
- mSearchManager = ((android.app.SearchManager) context.getSystemService(
- Context.SEARCH_SERVICE));
- }
-
- public static SearchManagerHelper getInstance(Context context) {
- synchronized (sLock) {
- if (sInstance == null) {
- sInstance = new SearchManagerHelper(context.getApplicationContext());
- }
- return sInstance;
- }
- }
-
- public void launchAssistAction() {
- try {
- SearchManager.class.getDeclaredMethod("launchLegacyAssist", String.class, Integer.TYPE,
- Bundle.class).invoke(mSearchManager, null, UserHandle.myUserId(), null);
- } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException
- | InvocationTargetException e) {
- Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e);
- }
- }
-}
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 8223a81c..0dabe7c0 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -114,7 +114,7 @@ public class SetupUtils {
@Override
public void onLoadFinished() {
manager.removeListener(this);
- updateChannelBrowsable(mTvApplication, inputId, postRunnable);
+ updateChannelsAfterSetup(mTvApplication, inputId, postRunnable);
}
@Override
@@ -124,17 +124,18 @@ public class SetupUtils {
public void onChannelBrowsableChanged() { }
});
} else {
- updateChannelBrowsable(mTvApplication, inputId, postRunnable);
+ updateChannelsAfterSetup(mTvApplication, inputId, postRunnable);
}
}
- private static void updateChannelBrowsable(Context context, final String inputId,
+ private static void updateChannelsAfterSetup(Context context, final String inputId,
final Runnable postRunnable) {
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
final ChannelDataManager manager = appSingletons.getChannelDataManager();
manager.updateChannels(new Runnable() {
@Override
public void run() {
+ Channel firstChannelForInput = null;
boolean browsableChanged = false;
for (Channel channel : manager.getChannelList()) {
if (channel.getInputId().equals(inputId)) {
@@ -142,8 +143,14 @@ public class SetupUtils {
manager.updateBrowsable(channel.getId(), true, true);
browsableChanged = true;
}
+ if (firstChannelForInput == null) {
+ firstChannelForInput = channel;
+ }
}
}
+ if (firstChannelForInput != null) {
+ Utils.setLastWatchedChannel(context, firstChannelForInput);
+ }
if (browsableChanged) {
manager.notifyChannelBrowsableChanged();
manager.applyUpdatedValuesToDb();
@@ -385,10 +392,7 @@ public class SetupUtils {
// 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();
- }
+ EpgFetcher.getInstance(context).startImmediately(true);
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java
index 15571e75..659807e2 100644
--- a/src/com/android/tv/tuner/util/StringUtils.java
+++ b/src/com/android/tv/util/StringUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.util;
+package com.android.tv.util;
/**
* Utility class for handling {@link String}.
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 121f56ed..67970ed6 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -18,20 +18,26 @@ package com.android.tv.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Handler;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import com.android.tv.Features;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -42,14 +48,64 @@ import java.util.Map;
public class TvInputManagerHelper {
private static final String TAG = "TvInputManagerHelper";
private static final boolean DEBUG = false;
+
+ /**
+ * Types of HDMI device and bundled tuner.
+ */
+ public static final int TYPE_CEC_DEVICE = -2;
+ public static final int TYPE_BUNDLED_TUNER = -3;
+ public static final int TYPE_CEC_DEVICE_RECORDER = -4;
+ public static final int TYPE_CEC_DEVICE_PLAYBACK = -5;
+ public static final int TYPE_MHL_MOBILE = -6;
+
+ private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
+ private static final String [] mPhysicalTunerBlackList = {
+ };
+ private static final String META_LABEL_SORT_KEY = "input_sort_key";
+
+ /**
+ * The default tv input priority to show.
+ */
+ private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
+ static {
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER);
+ }
+
private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = {
};
+ private static final String[] TESTABLE_INPUTS = {
+ "com.android.tv.testinput/.TestTvInputService"
+ };
+
private final Context mContext;
+ private final PackageManager mPackageManager;
private final TvInputManager mTvInputManager;
private final Map<String, Integer> mInputStateMap = new HashMap<>();
private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
+ private final Map<String, String> mTvInputLabels = new ArrayMap<>();
+ private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
+
+ private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>();
+
private final TvInputCallback mInternalCallback = new TvInputCallback() {
@Override
public void onInputStateChanged(String inputId, int state) {
@@ -72,6 +128,11 @@ public class TvInputManagerHelper {
TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
if (info != null) {
mInputMap.put(inputId, info);
+ mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
+ }
mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId));
mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info));
}
@@ -85,6 +146,11 @@ public class TvInputManagerHelper {
public void onInputRemoved(String inputId) {
if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
mInputMap.remove(inputId);
+ mTvInputLabels.remove(inputId);
+ mTvInputCustomLabels.remove(inputId);
+ mTvInputApplicationLabels.remove(inputId);
+ mTvInputApplicationIcons.remove(inputId);
+ mTvInputAppliactionBanners.remove(inputId);
mInputStateMap.remove(inputId);
mInputIdToPartnerInputMap.remove(inputId);
mContentRatingsManager.update();
@@ -103,6 +169,14 @@ public class TvInputManagerHelper {
}
TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
mInputMap.put(inputId, info);
+ mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
+ }
+ mTvInputApplicationLabels.remove(inputId);
+ mTvInputApplicationIcons.remove(inputId);
+ mTvInputAppliactionBanners.remove(inputId);
for (TvInputCallback callback : mCallbacks) {
callback.onInputUpdated(inputId);
}
@@ -114,6 +188,11 @@ public class TvInputManagerHelper {
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
mInputMap.put(inputInfo.getId(), inputInfo);
+ mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString());
+ }
for (TvInputCallback callback : mCallbacks) {
callback.onTvInputInfoUpdated(inputInfo);
}
@@ -131,6 +210,7 @@ public class TvInputManagerHelper {
public TvInputManagerHelper(Context context) {
mContext = context.getApplicationContext();
+ mPackageManager = context.getPackageManager();
mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
mContentRatingsManager = new ContentRatingsManager(context);
mParentalControlSettings = new ParentalControlSettings(context);
@@ -145,6 +225,11 @@ public class TvInputManagerHelper {
mStarted = true;
mTvInputManager.registerCallback(mInternalCallback, mHandler);
mInputMap.clear();
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ mTvInputApplicationIcons.clear();
+ mTvInputAppliactionBanners.clear();
mInputStateMap.clear();
mInputIdToPartnerInputMap.clear();
for (TvInputInfo input : mTvInputManager.getTvInputList()) {
@@ -171,9 +256,23 @@ public class TvInputManagerHelper {
mStarted = false;
mInputStateMap.clear();
mInputMap.clear();
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ mTvInputApplicationIcons.clear();
+ mTvInputAppliactionBanners.clear();;
mInputIdToPartnerInputMap.clear();
}
+ /**
+ * Clears the TvInput labels map.
+ */
+ public void clearTvInputLabels() {
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ }
+
public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) {
ArrayList<TvInputInfo> list = new ArrayList<>();
for (Map.Entry<String, Integer> pair : mInputStateMap.entrySet()) {
@@ -245,7 +344,69 @@ public class TvInputManagerHelper {
*/
@VisibleForTesting
public String loadLabel(TvInputInfo info) {
- return info.loadLabel(mContext).toString();
+ String label = mTvInputLabels.get(info.getId());
+ if (label == null) {
+ label = info.loadLabel(mContext).toString();
+ mTvInputLabels.put(info.getId(), label);
+ }
+ return label;
+ }
+
+ /**
+ * Loads custom label of {@code info}
+ */
+ public String loadCustomLabel(TvInputInfo info) {
+ String customLabel = mTvInputCustomLabels.get(info.getId());
+ if (customLabel == null) {
+ CharSequence customLabelCharSequence = info.loadCustomLabel(mContext);
+ if (customLabelCharSequence != null) {
+ customLabel = customLabelCharSequence.toString();
+ mTvInputCustomLabels.put(info.getId(), customLabel);
+ }
+ }
+ return customLabel;
+ }
+
+ /**
+ * Gets the tv input application's label.
+ */
+ public CharSequence getTvInputApplicationLabel(CharSequence inputId) {
+ return mTvInputApplicationLabels.get(inputId);
+ }
+
+ /**
+ * Stores the tv input application's label.
+ */
+ public void setTvInputApplicationLabel(String inputId, CharSequence label) {
+ mTvInputApplicationLabels.put(inputId, label);
+ }
+
+ /**
+ * Gets the tv input application's icon.
+ */
+ public Drawable getTvInputApplicationIcon(String inputId) {
+ return mTvInputApplicationIcons.get(inputId);
+ }
+
+ /**
+ * Stores the tv input application's icon.
+ */
+ public void setTvInputApplicationIcon(String inputId, Drawable icon) {
+ mTvInputApplicationIcons.put(inputId, icon);
+ }
+
+ /**
+ * Gets the tv input application's banner.
+ */
+ public Drawable getTvInputApplicationBanner(String inputId) {
+ return mTvInputAppliactionBanners.get(inputId);
+ }
+
+ /**
+ * Stores the tv input application's banner.
+ */
+ public void setTvInputApplicationBanner(String inputId, Drawable banner) {
+ mTvInputAppliactionBanners.put(inputId, banner);
}
/**
@@ -321,15 +482,55 @@ public class TvInputManagerHelper {
return mContentRatingsManager;
}
- private boolean isInBlackList(String inputId) {
- if (!Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) {
+ private int getInputSortKey(TvInputInfo input) {
+ return input.getServiceInfo().metaData.getInt(META_LABEL_SORT_KEY,
+ Integer.MAX_VALUE);
+ }
+
+ private boolean isInputPhysicalTuner(TvInputInfo input) {
+ String packageName = input.getServiceInfo().packageName;
+ if (Arrays.asList(mPhysicalTunerBlackList).contains(packageName)) {
return false;
}
- for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) {
- if (inputId.contains(disabledTunerInputPrefix)) {
- return true;
+
+ if (input.createSetupIntent() == null) {
+ return false;
+ } else {
+ boolean mayBeTunerInput = mPackageManager.checkPermission(
+ PERMISSION_ACCESS_ALL_EPG_DATA, input.getServiceInfo().packageName)
+ == PackageManager.PERMISSION_GRANTED;
+ if (!mayBeTunerInput) {
+ try {
+ ApplicationInfo ai = mPackageManager.getApplicationInfo(
+ input.getServiceInfo().packageName, 0);
+ if ((ai.flags & (ApplicationInfo.FLAG_SYSTEM
+ | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 0) {
+ return false;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
}
}
+ return true;
+ }
+
+ private boolean isInBlackList(String inputId) {
+ if (Features.USE_PARTNER_INPUT_BLACKLIST.isEnabled(mContext)) {
+ for (String disabledTunerInputPrefix : PARTNER_TUNER_INPUT_PREFIX_BLACKLIST) {
+ if (inputId.contains(disabledTunerInputPrefix)) {
+ return true;
+ }
+ }
+ }
+ if (TvCommonUtils.isRunningInTest()) {
+ for (String testableInput : TESTABLE_INPUTS) {
+ if (testableInput.equals(inputId)) {
+ return false;
+ }
+ }
+ return true;
+ }
return false;
}
@@ -357,4 +558,123 @@ public class TvInputManagerHelper {
return mInputManager.loadLabel(lhs).compareTo(mInputManager.loadLabel(rhs));
}
}
+
+ /**
+ * A comparator used for {@link com.android.tv.ui.SelectInputView} to show the list of
+ * TV inputs.
+ */
+ public static class InputComparator implements Comparator<TvInputInfo> {
+ private Map<Integer, Integer> mTypePriorities = new HashMap<>();
+ private final TvInputManagerHelper mTvInputManagerHelper;
+ private final Context mContext;
+
+ public InputComparator(Context context, TvInputManagerHelper tvInputManagerHelper) {
+ mContext = context;
+ mTvInputManagerHelper = tvInputManagerHelper;
+ setupDeviceTypePriorities();
+ }
+
+ @Override
+ public int compare(TvInputInfo lhs, TvInputInfo rhs) {
+ if (lhs == null) {
+ return (rhs == null) ? 0 : 1;
+ }
+ if (rhs == null) {
+ return -1;
+ }
+
+ boolean enabledL = (mTvInputManagerHelper.getInputState(lhs)
+ != TvInputManager.INPUT_STATE_DISCONNECTED);
+ boolean enabledR = (mTvInputManagerHelper.getInputState(rhs)
+ != TvInputManager.INPUT_STATE_DISCONNECTED);
+ if (enabledL != enabledR) {
+ return enabledL ? -1 : 1;
+ }
+
+ int priorityL = getPriority(lhs);
+ int priorityR = getPriority(rhs);
+ if (priorityL != priorityR) {
+ return priorityL - priorityR;
+ }
+
+ if (lhs.getType() == TvInputInfo.TYPE_TUNER
+ && rhs.getType() == TvInputInfo.TYPE_TUNER) {
+ boolean isPhysicalL = mTvInputManagerHelper.isInputPhysicalTuner(lhs);
+ boolean isPhysicalR = mTvInputManagerHelper.isInputPhysicalTuner(rhs);
+ if (isPhysicalL != isPhysicalR) {
+ return isPhysicalL ? -1 : 1;
+ }
+ }
+
+ int sortKeyL = mTvInputManagerHelper.getInputSortKey(lhs);
+ int sortKeyR = mTvInputManagerHelper.getInputSortKey(rhs);
+ if (sortKeyL != sortKeyR) {
+ return sortKeyR - sortKeyL;
+ }
+
+ String parentLabelL = lhs.getParentId() != null
+ ? getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getParentId()))
+ : getLabel(mTvInputManagerHelper.getTvInputInfo(lhs.getId()));
+ String parentLabelR = rhs.getParentId() != null
+ ? getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getParentId()))
+ : getLabel(mTvInputManagerHelper.getTvInputInfo(rhs.getId()));
+
+ if (!TextUtils.equals(parentLabelL, parentLabelR)) {
+ return parentLabelL.compareToIgnoreCase(parentLabelR);
+ }
+ return getLabel(lhs).compareToIgnoreCase(getLabel(rhs));
+ }
+
+ private String getLabel(TvInputInfo input) {
+ if (input == null) {
+ return "";
+ }
+ String label = mTvInputManagerHelper.loadCustomLabel(input);
+ if (TextUtils.isEmpty(label)) {
+ label = mTvInputManagerHelper.loadLabel(input);
+ }
+ return label;
+ }
+
+ private int getPriority(TvInputInfo info) {
+ Integer priority = null;
+ if (mTypePriorities != null) {
+ priority = mTypePriorities.get(getTvInputTypeForPriority(info));
+ }
+ if (priority != null) {
+ return priority;
+ }
+ return Integer.MAX_VALUE;
+ }
+
+ private void setupDeviceTypePriorities() {
+ mTypePriorities = Partner.getInstance(mContext).getInputsOrderMap();
+
+ // Fill in any missing priorities in the map we got from the OEM
+ int priority = mTypePriorities.size();
+ for (int type : DEFAULT_TV_INPUT_PRIORITY) {
+ if (!mTypePriorities.containsKey(type)) {
+ mTypePriorities.put(type, priority++);
+ }
+ }
+ }
+
+ private int getTvInputTypeForPriority(TvInputInfo info) {
+ if (info.getHdmiDeviceInfo() != null) {
+ if (info.getHdmiDeviceInfo().isCecDevice()) {
+ switch (info.getHdmiDeviceInfo().getDeviceType()) {
+ case HdmiDeviceInfo.DEVICE_RECORDER:
+ return TYPE_CEC_DEVICE_RECORDER;
+ case HdmiDeviceInfo.DEVICE_PLAYBACK:
+ return TYPE_CEC_DEVICE_PLAYBACK;
+ default:
+ return TYPE_CEC_DEVICE;
+ }
+ } else if (info.getHdmiDeviceInfo().isMhlDevice()) {
+ return TYPE_MHL_MOBILE;
+ }
+ }
+ return info.getType();
+ }
+ }
}
diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java
index 97ff59d6..970cd055 100644
--- a/src/com/android/tv/util/TvSettings.java
+++ b/src/com/android/tv/util/TvSettings.java
@@ -17,6 +17,8 @@
package com.android.tv.util;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.media.tv.TvTrackInfo;
import android.preference.PreferenceManager;
import android.support.annotation.IntDef;
@@ -26,7 +28,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
-
/**
* A class about the constants for TV settings.
* Objects that are returned from the various {@code get} methods must be treated as immutable.
@@ -35,44 +36,21 @@ public final class TvSettings {
private TvSettings() {}
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
public static final String PREF_PIN = "pin"; // 4-digit string value. Otherwise, it's not set.
- // PIP sounds
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- PIP_SOUND_MAIN, PIP_SOUND_PIP_WINDOW })
- public @interface PipSound {}
- public static final int PIP_SOUND_MAIN = 0;
- public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1;
-
- // PIP layouts
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({
- PIP_LAYOUT_BOTTOM_RIGHT, PIP_LAYOUT_TOP_RIGHT, PIP_LAYOUT_TOP_LEFT,
- PIP_LAYOUT_BOTTOM_LEFT, PIP_LAYOUT_SIDE_BY_SIDE })
- public @interface PipLayout {}
- public static final int PIP_LAYOUT_BOTTOM_RIGHT = 0;
- public static final int PIP_LAYOUT_TOP_RIGHT = PIP_LAYOUT_BOTTOM_RIGHT + 1;
- public static final int PIP_LAYOUT_TOP_LEFT = PIP_LAYOUT_TOP_RIGHT + 1;
- public static final int PIP_LAYOUT_BOTTOM_LEFT = PIP_LAYOUT_TOP_LEFT + 1;
- public static final int PIP_LAYOUT_SIDE_BY_SIDE = PIP_LAYOUT_BOTTOM_LEFT + 1;
- public static final int PIP_LAYOUT_LAST = PIP_LAYOUT_SIDE_BY_SIDE;
-
- // PIP sizes
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({ PIP_SIZE_SMALL, PIP_SIZE_BIG })
- public @interface PipSize {}
- public static final int PIP_SIZE_SMALL = 0;
- public static final int PIP_SIZE_BIG = PIP_SIZE_SMALL + 1;
- public static final int PIP_SIZE_LAST = PIP_SIZE_BIG;
-
// Multi-track audio settings
private static final String PREF_MULTI_AUDIO_ID = "pref.multi_audio_id";
private static final String PREF_MULTI_AUDIO_LANGUAGE = "pref.multi_audio_language";
private static final String PREF_MULTI_AUDIO_CHANNEL_COUNT = "pref.multi_audio_channel_count";
+ // DVR Multi-audio and subtitle settings
+ private static final String PREF_DVR_MULTI_AUDIO_ID = "pref.dvr_multi_audio_id";
+ private static final String PREF_DVR_MULTI_AUDIO_LANGUAGE = "pref.dvr_multi_audio_language";
+ private static final String PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT =
+ "pref.dvr_multi_audio_channel_count";
+ private static final String PREF_DVR_SUBTITLE_ID = "pref.dvr_subtitle_id";
+ private static final String PREF_DVR_SUBTITLE_LANGUAGE = "pref.dvr_subtitle_language";
+
// Parental Control settings
private static final String PREF_CONTENT_RATING_SYSTEMS = "pref.content_rating_systems";
private static final String PREF_CONTENT_RATING_LEVEL = "pref.content_rating_level";
@@ -89,59 +67,6 @@ public final class TvSettings {
public static final int CONTENT_RATING_LEVEL_LOW = 3;
public static final int CONTENT_RATING_LEVEL_CUSTOM = 4;
- // PIP settings
- /**
- * Returns the layout of the PIP window stored in the shared preferences.
- *
- * @return the saved layout of the PIP window. This value is one of
- * {@link #PIP_LAYOUT_TOP_LEFT}, {@link #PIP_LAYOUT_TOP_RIGHT},
- * {@link #PIP_LAYOUT_BOTTOM_LEFT}, {@link #PIP_LAYOUT_BOTTOM_RIGHT} and
- * {@link #PIP_LAYOUT_SIDE_BY_SIDE}. If the preference value does not exist,
- * {@link #PIP_LAYOUT_BOTTOM_RIGHT} is returned.
- */
- @SuppressWarnings("ResourceType")
- @PipLayout
- public static int getPipLayout(Context context) {
- return PreferenceManager.getDefaultSharedPreferences(context).getInt(
- PREF_PIP_LAYOUT, PIP_LAYOUT_BOTTOM_RIGHT);
- }
-
- /**
- * Stores the layout of PIP window to the shared preferences.
- *
- * @param pipLayout This value should be one of {@link #PIP_LAYOUT_TOP_LEFT},
- * {@link #PIP_LAYOUT_TOP_RIGHT}, {@link #PIP_LAYOUT_BOTTOM_LEFT},
- * {@link #PIP_LAYOUT_BOTTOM_RIGHT} and {@link #PIP_LAYOUT_SIDE_BY_SIDE}.
- */
- public static void setPipLayout(Context context, @PipLayout int pipLayout) {
- PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(
- PREF_PIP_LAYOUT, pipLayout).apply();
- }
-
- /**
- * Returns the size of the PIP view stored in the shared preferences.
- *
- * @return the saved size of the PIP view. This value is one of
- * {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}. If the preference value does not
- * exist, {@link #PIP_SIZE_SMALL} is returned.
- */
- @SuppressWarnings("ResourceType")
- @PipSize
- public static int getPipSize(Context context) {
- return PreferenceManager.getDefaultSharedPreferences(context).getInt(
- PREF_PIP_SIZE, PIP_SIZE_SMALL);
- }
-
- /**
- * Stores the size of PIP view to the shared preferences.
- *
- * @param pipSize This value should be one of {@link #PIP_SIZE_SMALL} and {@link #PIP_SIZE_BIG}.
- */
- public static void setPipSize(Context context, @PipSize int pipSize) {
- PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(
- PREF_PIP_SIZE, pipSize).apply();
- }
-
// Multi-track audio settings
public static String getMultiAudioId(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString(
@@ -173,6 +98,57 @@ public final class TvSettings {
PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply();
}
+ public static void setDvrPlaybackTrackSettings(Context context, int trackType,
+ TvTrackInfo info) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ if (info == null) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .remove(PREF_DVR_MULTI_AUDIO_ID).apply();
+ } else {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(PREF_DVR_MULTI_AUDIO_LANGUAGE, info.getLanguage())
+ .putInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, info.getAudioChannelCount())
+ .putString(PREF_DVR_MULTI_AUDIO_ID, info.getId()).apply();
+ }
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ if (info == null) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .remove(PREF_DVR_SUBTITLE_ID).apply();
+ } else {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(PREF_DVR_SUBTITLE_LANGUAGE, info.getLanguage())
+ .putString(PREF_DVR_SUBTITLE_ID, info.getId()).apply();
+ }
+ }
+ }
+
+ public static TvTrackInfo getDvrPlaybackTrackSettings(Context context,
+ int trackType) {
+ String language;
+ String trackId;
+ int channelCount;
+ SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ trackId = pref.getString(PREF_DVR_MULTI_AUDIO_ID, null);
+ if (trackId == null) {
+ return null;
+ }
+ language = pref.getString(PREF_DVR_MULTI_AUDIO_LANGUAGE, null);
+ channelCount = pref.getInt(PREF_DVR_MULTI_AUDIO_CHANNEL_COUNT, 0);
+ return new TvTrackInfo.Builder(trackType, trackId)
+ .setLanguage(language).setAudioChannelCount(channelCount).build();
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ trackId = pref.getString(PREF_DVR_SUBTITLE_ID, null);
+ if (trackId == null) {
+ return null;
+ }
+ language = pref.getString(PREF_DVR_SUBTITLE_LANGUAGE, null);
+ return new TvTrackInfo.Builder(trackType, trackId).setLanguage(language).build();
+ } else {
+ return null;
+ }
+ }
+
// Parental Control settings
public static void addContentRatingSystems(Context context, Set<String> ids) {
Set<String> contentRatingSystemSet = getContentRatingSystemSet(context);
@@ -254,4 +230,4 @@ public final class TvSettings {
PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
PREF_DISABLE_PIN_UNTIL, timeMillis).apply();
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/util/TvTrackInfoUtils.java b/src/com/android/tv/util/TvTrackInfoUtils.java
index c004f001..667cc9bf 100644
--- a/src/com/android/tv/util/TvTrackInfoUtils.java
+++ b/src/com/android/tv/util/TvTrackInfoUtils.java
@@ -52,35 +52,22 @@ public class TvTrackInfoUtils {
}
// Assumes {@code null} language matches to any language since it means user hasn't
// selected any track before or selected a track without language information.
- boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(),
- language);
boolean lhsLangMatch = language == null || Utils.isEqualLanguage(lhs.getLanguage(),
language);
- if (rhsLangMatch) {
- if (lhsLangMatch) {
- boolean rhsCountMatch = rhs.getAudioChannelCount() == channelCount;
- boolean lhsCountMatch = lhs.getAudioChannelCount() == channelCount;
- if (rhsCountMatch) {
- if (lhsCountMatch) {
- boolean rhsIdMatch = rhs.getId().equals(id);
- boolean lhsIdMatch = lhs.getId().equals(id);
- if (rhsIdMatch) {
- return lhsIdMatch ? 0 : -1;
- } else {
- return lhsIdMatch ? 1 : 0;
- }
-
- } else {
- return -1;
- }
- } else {
- return lhsCountMatch ? 1 : 0;
- }
+ boolean rhsLangMatch = language == null || Utils.isEqualLanguage(rhs.getLanguage(),
+ language);
+ if (lhsLangMatch && rhsLangMatch) {
+ boolean lhsCountMatch = lhs.getType() != TvTrackInfo.TYPE_AUDIO
+ || lhs.getAudioChannelCount() == channelCount;
+ boolean rhsCountMatch = rhs.getType() != TvTrackInfo.TYPE_AUDIO
+ || rhs.getAudioChannelCount() == channelCount;
+ if (lhsCountMatch && rhsCountMatch) {
+ return Boolean.compare(lhs.getId().equals(id), rhs.getId().equals(id));
} else {
- return -1;
+ return Boolean.compare(lhsCountMatch, rhsCountMatch);
}
} else {
- return lhsLangMatch ? 1 : 0;
+ return Boolean.compare(lhsLangMatch, rhsLangMatch);
}
}
};
@@ -112,4 +99,4 @@ public class TvTrackInfoUtils {
private TvTrackInfoUtils() {
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 99d34431..3fe2ec3d 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -44,11 +44,13 @@ import android.view.View;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.BuildConfig;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.GenreItems;
import com.android.tv.data.Program;
import com.android.tv.data.StreamInfo;
+import com.android.tv.experiments.Experiments;
import java.io.File;
import java.text.SimpleDateFormat;
@@ -83,9 +85,6 @@ public class Utils {
public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED =
"recorded_program_pin_checked";
- // Query parameter in the intent of starting MainActivity.
- public static final String PARAM_SOURCE = "source";
-
private static final String PATH_CHANNEL = "channel";
private static final String PATH_PROGRAM = "program";
@@ -97,6 +96,8 @@ public class Utils {
"last_watched_tuner_input_id";
private static final String PREF_KEY_RECORDING_FAILED_REASONS =
"recording_failed_reasons";
+ private static final String PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET =
+ "failed_scheduled_recording_info_set";
private static final int VIDEO_SD_WIDTH = 704;
private static final int VIDEO_SD_HEIGHT = 480;
@@ -114,6 +115,7 @@ public class Utils {
private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
private static final long RECORDING_FAILED_REASON_NONE = 0;
+ private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30);
private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
// Hardcoded list for known bundled inputs not written by OEM/SOCs.
@@ -207,6 +209,28 @@ public class Utils {
}
/**
+ * Adds the info of failed scheduled recording.
+ */
+ public static void addFailedScheduledRecordingInfo(Context context,
+ String scheduledRecordingInfo) {
+ Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context);
+ failedScheduledRecordingInfoSet.add(scheduledRecordingInfo);
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET,
+ failedScheduledRecordingInfoSet)
+ .apply();
+ }
+
+ /**
+ * Clears the failed scheduled recording info set.
+ */
+ public static void clearFailedScheduledRecordingInfoSet(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET)
+ .apply();
+ }
+
+ /**
* Clears recording failed reason.
*/
public static void clearRecordingFailedReason(Context context, int reason) {
@@ -246,6 +270,14 @@ public class Utils {
}
/**
+ * Returns the failed scheduled recordings info set.
+ */
+ public static Set<String> getFailedScheduledRecordingInfoSet(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>());
+ }
+
+ /**
* Checks do recording failed reason exist.
*/
public static boolean hasRecordingFailedReason(Context context, int reason) {
@@ -333,6 +365,14 @@ public class Utils {
}
/**
+ * Returns the round off minutes when convert milliseconds to minutes.
+ */
+ public static int getRoundOffMinsFromMs(long millis) {
+ // Round off the result by adding half minute to the original ms.
+ return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS);
+ }
+
+ /**
* Returns duration string according to the date & time format.
* If {@code startUtcMillis} and {@code endUtcMills} are equal,
* formatted time will be returned instead.
@@ -392,16 +432,18 @@ public class Utils {
: DateUtils.formatDateRange(context, startUtcMillis, endUtcMillis + 1, flag);
}
- @VisibleForTesting
+ /**
+ * Checks if two given time (in milliseconds) are in the same day with regard to the
+ * locale timezone.
+ */
public static boolean isInGivenDay(long dayToMatchInMillis, long subjectTimeInMillis) {
- final long DAY_IN_MS = TimeUnit.DAYS.toMillis(1);
TimeZone timeZone = Calendar.getInstance().getTimeZone();
long offset = timeZone.getRawOffset();
if (timeZone.inDaylightTime(new Date(dayToMatchInMillis))) {
offset += timeZone.getDSTSavings();
}
- return Utils.floorTime(dayToMatchInMillis + offset, DAY_IN_MS)
- == Utils.floorTime(subjectTimeInMillis + offset, DAY_IN_MS);
+ return Utils.floorTime(dayToMatchInMillis + offset, ONE_DAY_MS)
+ == Utils.floorTime(subjectTimeInMillis + offset, ONE_DAY_MS);
}
/**
@@ -523,7 +565,7 @@ public class Utils {
if (track.getType() != TvTrackInfo.TYPE_AUDIO) {
throw new IllegalArgumentException("Not an audio track: " + track);
}
- String language = context.getString(R.string.default_language);
+ String language = context.getString(R.string.multi_audio_unknown_language);
if (!TextUtils.isEmpty(track.getLanguage())) {
language = new Locale(track.getLanguage()).getDisplayName();
} else {
@@ -860,4 +902,11 @@ public class Utils {
}
return Genres.encode(genres);
}
+
+ /**
+ * Returns true if the current user is a developer.
+ */
+ public static boolean isDeveloper() {
+ return BuildConfig.ENG || Experiments.ENABLE_DEVELOPER_FEATURES.get();
+ }
}
diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java
new file mode 100644
index 00000000..113bda27
--- /dev/null
+++ b/src/com/android/tv/util/ViewCache.java
@@ -0,0 +1,70 @@
+package com.android.tv.util;
+
+import android.util.SparseArray;
+import android.view.View;
+
+import java.util.ArrayList;
+
+/**
+ * A cache for the views.
+ */
+public class ViewCache {
+ private final static SparseArray<ArrayList<View>> mViews = new SparseArray();
+
+ private static ViewCache sViewCache;
+
+ private ViewCache() { }
+
+ /**
+ * Returns an instance of the view cache.
+ */
+ public static ViewCache getInstance() {
+ if (sViewCache == null) {
+ return new ViewCache();
+ } else {
+ return sViewCache;
+ }
+ }
+
+ /**
+ * Returns if the view cache is empty.
+ */
+ public boolean isEmpty() {
+ return mViews.size() == 0;
+ }
+
+ /**
+ * Stores a view into this view cache.
+ */
+ public void putView(int resId, View view) {
+ ArrayList<View> views = mViews.get(resId);
+ if (views == null) {
+ views = new ArrayList();
+ mViews.put(resId, views);
+ }
+ views.add(view);
+ }
+
+ /**
+ * Returns the view for specific resource id.
+ */
+ public View getView(int resId) {
+ ArrayList<View> views = mViews.get(resId);
+ if (views != null && !views.isEmpty()) {
+ View view = views.remove(views.size() - 1);
+ if (views.isEmpty()) {
+ mViews.remove(resId);
+ }
+ return view;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Clears the view cache.
+ */
+ public void clear() {
+ mViews.clear();
+ }
+}