From 3dfa929b24f38ac7836450176d88ceab41dc6ac5 Mon Sep 17 00:00:00 2001 From: Nick Chalko Date: Thu, 4 May 2017 14:37:34 -0700 Subject: Sync to ub-tv-dev at f0024d79653da8c8999a91f995431a645a6ff4a2 Change-Id: I4199ec04cacb4a78be58b85302a39d917658dc28 --- src/com/android/tv/Features.java | 64 ++ src/com/android/tv/InputSessionManager.java | 28 +- src/com/android/tv/MainActivity.java | 764 +++++------------ src/com/android/tv/SetupPassthroughActivity.java | 90 +- src/com/android/tv/TimeShiftManager.java | 9 +- src/com/android/tv/TvApplication.java | 19 +- src/com/android/tv/TvOptionsManager.java | 133 +-- src/com/android/tv/analytics/DurationTimer.java | 62 -- src/com/android/tv/data/Channel.java | 97 ++- src/com/android/tv/data/ChannelDataManager.java | 42 +- src/com/android/tv/data/ChannelLogoFetcher.java | 307 ++----- src/com/android/tv/data/ChannelNumber.java | 71 +- src/com/android/tv/data/InternalDataUtils.java | 2 +- src/com/android/tv/data/StreamInfo.java | 4 + src/com/android/tv/data/epg/EpgFetcher.java | 327 +++++--- src/com/android/tv/data/epg/EpgReader.java | 29 +- src/com/android/tv/data/epg/StubEpgReader.java | 17 +- .../tv/dialog/DvrHistoryDialogFragment.java | 129 +++ .../tv/dialog/FullscreenDialogFragment.java | 2 +- .../android/tv/dialog/HalfSizedDialogFragment.java | 123 +++ .../tv/dialog/SafeDismissDialogFragment.java | 26 - src/com/android/tv/dialog/WebDialogFragment.java | 17 +- src/com/android/tv/dvr/BaseDvrDataManager.java | 41 +- src/com/android/tv/dvr/ConflictChecker.java | 277 ------- src/com/android/tv/dvr/DvrDataManager.java | 12 +- src/com/android/tv/dvr/DvrDataManagerImpl.java | 143 +++- src/com/android/tv/dvr/DvrDbSync.java | 363 --------- src/com/android/tv/dvr/DvrManager.java | 81 +- src/com/android/tv/dvr/DvrPlaybackActivity.java | 67 -- .../tv/dvr/DvrPlaybackMediaSessionHelper.java | 327 -------- src/com/android/tv/dvr/DvrPlayer.java | 425 ---------- src/com/android/tv/dvr/DvrRecordingService.java | 122 --- src/com/android/tv/dvr/DvrScheduleManager.java | 139 ++-- .../android/tv/dvr/DvrStartRecordingReceiver.java | 34 - .../android/tv/dvr/DvrStorageStatusManager.java | 35 +- src/com/android/tv/dvr/DvrUiHelper.java | 450 ---------- .../android/tv/dvr/DvrWatchedPositionManager.java | 1 + .../android/tv/dvr/EpisodicProgramLoadTask.java | 382 --------- src/com/android/tv/dvr/IdGenerator.java | 50 -- src/com/android/tv/dvr/InputTaskScheduler.java | 431 ---------- src/com/android/tv/dvr/RecordedProgram.java | 868 -------------------- src/com/android/tv/dvr/RecordingTask.java | 519 ------------ src/com/android/tv/dvr/ScheduledProgramReaper.java | 67 -- src/com/android/tv/dvr/ScheduledRecording.java | 887 -------------------- src/com/android/tv/dvr/Scheduler.java | 283 ------- src/com/android/tv/dvr/SeriesInfo.java | 76 -- src/com/android/tv/dvr/SeriesRecording.java | 755 ----------------- .../android/tv/dvr/SeriesRecordingScheduler.java | 579 ------------- src/com/android/tv/dvr/WritableDvrDataManager.java | 6 +- src/com/android/tv/dvr/data/IdGenerator.java | 50 ++ src/com/android/tv/dvr/data/RecordedProgram.java | 868 ++++++++++++++++++++ .../android/tv/dvr/data/ScheduledRecording.java | 902 +++++++++++++++++++++ .../android/tv/dvr/data/SeasonEpisodeNumber.java | 72 ++ src/com/android/tv/dvr/data/SeriesInfo.java | 76 ++ src/com/android/tv/dvr/data/SeriesRecording.java | 756 +++++++++++++++++ .../android/tv/dvr/provider/AsyncDvrDbTask.java | 4 +- .../android/tv/dvr/provider/DvrDatabaseHelper.java | 4 +- src/com/android/tv/dvr/provider/DvrDbSync.java | 373 +++++++++ .../tv/dvr/provider/EpisodicProgramLoadTask.java | 329 ++++++++ .../android/tv/dvr/recorder/ConflictChecker.java | 280 +++++++ .../tv/dvr/recorder/DvrRecordingService.java | 154 ++++ .../tv/dvr/recorder/DvrStartRecordingReceiver.java | 34 + .../tv/dvr/recorder/InputTaskScheduler.java | 435 ++++++++++ src/com/android/tv/dvr/recorder/RecordingTask.java | 530 ++++++++++++ .../tv/dvr/recorder/ScheduledProgramReaper.java | 70 ++ src/com/android/tv/dvr/recorder/Scheduler.java | 287 +++++++ .../tv/dvr/recorder/SeriesRecordingScheduler.java | 562 +++++++++++++ .../android/tv/dvr/ui/ActionPresenterSelector.java | 138 ---- src/com/android/tv/dvr/ui/BigArguments.java | 54 ++ .../ui/ChangeImageTransformWithScaledParent.java | 79 ++ .../tv/dvr/ui/CurrentRecordingDetailsFragment.java | 59 -- src/com/android/tv/dvr/ui/DetailsContent.java | 207 ----- .../android/tv/dvr/ui/DetailsContentPresenter.java | 300 ------- .../tv/dvr/ui/DetailsViewBackgroundHelper.java | 92 --- src/com/android/tv/dvr/ui/DvrActivity.java | 35 - .../tv/dvr/ui/DvrAlreadyRecordedFragment.java | 4 +- .../tv/dvr/ui/DvrAlreadyScheduledFragment.java | 5 +- src/com/android/tv/dvr/ui/DvrBrowseFragment.java | 601 -------------- .../ui/DvrChannelRecordDurationOptionFragment.java | 2 +- src/com/android/tv/dvr/ui/DvrConflictFragment.java | 7 +- src/com/android/tv/dvr/ui/DvrDetailsActivity.java | 98 --- src/com/android/tv/dvr/ui/DvrDetailsFragment.java | 344 -------- .../tv/dvr/ui/DvrForgetStorageErrorFragment.java | 87 -- .../android/tv/dvr/ui/DvrGuidedStepFragment.java | 50 +- .../tv/dvr/ui/DvrHalfSizedDialogFragment.java | 12 + .../dvr/ui/DvrInsufficientSpaceErrorFragment.java | 84 +- src/com/android/tv/dvr/ui/DvrItemPresenter.java | 80 -- .../tv/dvr/ui/DvrMissingStorageErrorFragment.java | 50 +- .../tv/dvr/ui/DvrPlaybackCardPresenter.java | 82 -- .../tv/dvr/ui/DvrPlaybackControlHelper.java | 313 ------- .../tv/dvr/ui/DvrPlaybackOverlayFragment.java | 304 ------- .../tv/dvr/ui/DvrPrioritySettingsFragment.java | 250 ++++++ src/com/android/tv/dvr/ui/DvrScheduleFragment.java | 17 +- .../android/tv/dvr/ui/DvrSchedulesActivity.java | 104 --- .../tv/dvr/ui/DvrSeriesDeletionActivity.java | 5 +- .../tv/dvr/ui/DvrSeriesDeletionFragment.java | 253 ++++++ .../tv/dvr/ui/DvrSeriesScheduledFragment.java | 45 +- .../tv/dvr/ui/DvrSeriesSettingsActivity.java | 20 +- .../tv/dvr/ui/DvrSeriesSettingsFragment.java | 366 +++++++++ .../tv/dvr/ui/DvrStopRecordingFragment.java | 11 +- .../tv/dvr/ui/DvrStopSeriesRecordingFragment.java | 4 +- src/com/android/tv/dvr/ui/DvrUiHelper.java | 575 +++++++++++++ src/com/android/tv/dvr/ui/FadeBackground.java | 70 ++ .../android/tv/dvr/ui/FullScheduleCardHolder.java | 29 - .../tv/dvr/ui/FullSchedulesCardPresenter.java | 84 -- .../android/tv/dvr/ui/HalfSizedDialogFragment.java | 117 --- .../tv/dvr/ui/PrioritySettingsFragment.java | 251 ------ .../tv/dvr/ui/RecordedProgramDetailsFragment.java | 170 ---- .../tv/dvr/ui/RecordedProgramPresenter.java | 182 ----- src/com/android/tv/dvr/ui/RecordingCardView.java | 185 ----- .../tv/dvr/ui/RecordingDetailsFragment.java | 87 -- .../dvr/ui/ScheduledRecordingDetailsFragment.java | 97 --- .../tv/dvr/ui/ScheduledRecordingPresenter.java | 177 ---- .../android/tv/dvr/ui/SeriesDeletionFragment.java | 252 ------ .../tv/dvr/ui/SeriesRecordingDetailsFragment.java | 375 --------- .../tv/dvr/ui/SeriesRecordingPresenter.java | 234 ------ .../android/tv/dvr/ui/SeriesSettingsFragment.java | 397 --------- src/com/android/tv/dvr/ui/SortedArrayAdapter.java | 90 +- .../tv/dvr/ui/browse/ActionPresenterSelector.java | 134 +++ .../ui/browse/CurrentRecordingDetailsFragment.java | 120 +++ .../android/tv/dvr/ui/browse/DetailsContent.java | 207 +++++ .../tv/dvr/ui/browse/DetailsContentPresenter.java | 299 +++++++ .../dvr/ui/browse/DetailsViewBackgroundHelper.java | 92 +++ .../tv/dvr/ui/browse/DvrBrowseActivity.java | 35 + .../tv/dvr/ui/browse/DvrBrowseFragment.java | 634 +++++++++++++++ .../tv/dvr/ui/browse/DvrDetailsActivity.java | 98 +++ .../tv/dvr/ui/browse/DvrDetailsFragment.java | 344 ++++++++ .../android/tv/dvr/ui/browse/DvrItemPresenter.java | 83 ++ .../tv/dvr/ui/browse/DvrListRowPresenter.java | 34 + .../tv/dvr/ui/browse/FullScheduleCardHolder.java | 29 + .../dvr/ui/browse/FullSchedulesCardPresenter.java | 88 ++ .../ui/browse/RecordedProgramDetailsFragment.java | 170 ++++ .../tv/dvr/ui/browse/RecordedProgramPresenter.java | 179 ++++ .../tv/dvr/ui/browse/RecordingCardView.java | 264 ++++++ .../tv/dvr/ui/browse/RecordingDetailsFragment.java | 87 ++ .../browse/ScheduledRecordingDetailsFragment.java | 97 +++ .../dvr/ui/browse/ScheduledRecordingPresenter.java | 174 ++++ .../ui/browse/SeriesRecordingDetailsFragment.java | 369 +++++++++ .../tv/dvr/ui/browse/SeriesRecordingPresenter.java | 233 ++++++ .../tv/dvr/ui/list/BaseDvrSchedulesFragment.java | 2 +- .../tv/dvr/ui/list/DvrSchedulesActivity.java | 116 +++ .../tv/dvr/ui/list/DvrSchedulesFragment.java | 5 +- .../tv/dvr/ui/list/DvrSeriesSchedulesFragment.java | 72 +- .../android/tv/dvr/ui/list/EpisodicProgramRow.java | 6 +- src/com/android/tv/dvr/ui/list/ScheduleRow.java | 4 +- .../android/tv/dvr/ui/list/ScheduleRowAdapter.java | 4 +- .../tv/dvr/ui/list/ScheduleRowPresenter.java | 50 +- .../android/tv/dvr/ui/list/SchedulesHeaderRow.java | 20 +- .../dvr/ui/list/SchedulesHeaderRowPresenter.java | 26 +- .../tv/dvr/ui/list/SeriesScheduleRowAdapter.java | 24 +- .../tv/dvr/ui/list/SeriesScheduleRowPresenter.java | 17 +- .../tv/dvr/ui/playback/DvrPlaybackActivity.java | 67 ++ .../dvr/ui/playback/DvrPlaybackCardPresenter.java | 77 ++ .../dvr/ui/playback/DvrPlaybackControlHelper.java | 399 +++++++++ .../ui/playback/DvrPlaybackMediaSessionHelper.java | 335 ++++++++ .../ui/playback/DvrPlaybackOverlayFragment.java | 431 ++++++++++ .../dvr/ui/playback/DvrPlaybackSideFragment.java | 154 ++++ src/com/android/tv/dvr/ui/playback/DvrPlayer.java | 583 +++++++++++++ src/com/android/tv/experiments/ExperimentFlag.java | 32 +- src/com/android/tv/experiments/Experiments.java | 9 +- src/com/android/tv/guide/ProgramGuide.java | 2 +- src/com/android/tv/guide/ProgramItemView.java | 21 +- src/com/android/tv/guide/ProgramManager.java | 20 +- src/com/android/tv/guide/ProgramTableAdapter.java | 68 +- src/com/android/tv/guide/TimeListAdapter.java | 27 +- src/com/android/tv/menu/ActionCardView.java | 6 +- src/com/android/tv/menu/AppLinkCardView.java | 198 ++++- src/com/android/tv/menu/BaseCardView.java | 50 +- src/com/android/tv/menu/ChannelCardView.java | 8 +- src/com/android/tv/menu/ChannelsRow.java | 10 +- src/com/android/tv/menu/ChannelsRowAdapter.java | 4 +- src/com/android/tv/menu/ItemListRowView.java | 14 +- src/com/android/tv/menu/Menu.java | 49 +- src/com/android/tv/menu/MenuAction.java | 69 +- src/com/android/tv/menu/MenuLayoutManager.java | 20 +- src/com/android/tv/menu/MenuRowFactory.java | 24 +- src/com/android/tv/menu/MenuUpdater.java | 41 +- src/com/android/tv/menu/OptionsRowAdapter.java | 50 +- .../android/tv/menu/PartnerOptionsRowAdapter.java | 3 +- src/com/android/tv/menu/PipOptionsRowAdapter.java | 137 ---- src/com/android/tv/menu/PlayControlsButton.java | 24 +- src/com/android/tv/menu/PlayControlsRowView.java | 194 ++--- src/com/android/tv/menu/PlaybackProgressBar.java | 168 ++++ src/com/android/tv/menu/TvOptionsRowAdapter.java | 128 +-- .../tv/onboarding/SetupSourcesFragment.java | 2 + .../android/tv/receiver/BootCompletedReceiver.java | 2 +- src/com/android/tv/receiver/GlobalKeyReceiver.java | 40 +- .../tv/receiver/PackageIntentsReceiver.java | 6 + .../tv/recommendation/NotificationService.java | 1 + src/com/android/tv/search/DataManagerSearch.java | 4 +- src/com/android/tv/search/SearchInterface.java | 2 - src/com/android/tv/search/TvProviderSearch.java | 14 +- .../android/tv/tuner/ChannelScanFileParser.java | 2 +- src/com/android/tv/tuner/TunerHal.java | 43 +- src/com/android/tv/tuner/TunerInputController.java | 195 ++++- src/com/android/tv/tuner/TunerPreferences.java | 97 ++- src/com/android/tv/tuner/UsbTunerHal.java | 6 +- src/com/android/tv/tuner/cc/CaptionLayout.java | 2 +- .../android/tv/tuner/cc/CaptionTrackRenderer.java | 2 +- src/com/android/tv/tuner/cc/Cea708Parser.java | 5 +- src/com/android/tv/tuner/data/PsiData.java | 4 +- src/com/android/tv/tuner/data/PsipData.java | 6 +- src/com/android/tv/tuner/data/TunerChannel.java | 130 ++- .../tuner/exoplayer/ExoPlayerSampleExtractor.java | 378 ++++++--- .../tv/tuner/exoplayer/FileSampleExtractor.java | 14 +- .../android/tv/tuner/exoplayer/MpegTsPlayer.java | 62 +- .../tv/tuner/exoplayer/MpegTsRendererBuilder.java | 13 +- .../exoplayer/ac3/Ac3DefaultTrackRenderer.java | 602 ++++++++++++++ .../exoplayer/ac3/Ac3MediaCodecTrackRenderer.java | 97 +++ .../exoplayer/ac3/Ac3PassthroughTrackRenderer.java | 540 ------------ .../tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java | 94 --- .../tv/tuner/exoplayer/ac3/AudioTrackMonitor.java | 4 +- .../tv/tuner/exoplayer/ac3/AudioTrackWrapper.java | 20 +- .../tv/tuner/exoplayer/buffer/BufferManager.java | 280 ++++--- .../tuner/exoplayer/buffer/DvrStorageManager.java | 209 +++-- .../exoplayer/buffer/RecordingSampleBuffer.java | 23 +- .../tv/tuner/exoplayer/buffer/SampleChunk.java | 23 +- .../exoplayer/buffer/SampleChunkIoHelper.java | 115 ++- .../tv/tuner/exoplayer/buffer/SampleQueue.java | 1 + .../tuner/exoplayer/buffer/SimpleSampleBuffer.java | 1 + .../exoplayer/buffer/TrickplayStorageManager.java | 85 +- .../tv/tuner/setup/ConnectionTypeFragment.java | 18 + .../android/tv/tuner/setup/PostalCodeFragment.java | 184 +++++ src/com/android/tv/tuner/setup/ScanFragment.java | 58 +- .../android/tv/tuner/setup/ScanResultFragment.java | 17 +- .../android/tv/tuner/setup/TunerSetupActivity.java | 238 +++++- .../android/tv/tuner/setup/WelcomeFragment.java | 38 +- .../android/tv/tuner/source/FileTsStreamer.java | 2 +- .../tv/tuner/source/TsDataSourceManager.java | 12 +- .../android/tv/tuner/source/TunerTsStreamer.java | 58 +- .../tv/tuner/source/TunerTsStreamerManager.java | 26 +- src/com/android/tv/tuner/ts/SectionParser.java | 20 +- src/com/android/tv/tuner/ts/TsParser.java | 18 + .../tv/tuner/tvinput/ChannelDataManager.java | 54 +- .../android/tv/tuner/tvinput/EventDetector.java | 52 +- .../tv/tuner/tvinput/FileSourceEventDetector.java | 4 +- .../tuner/tvinput/TunerRecordingSessionWorker.java | 116 ++- src/com/android/tv/tuner/tvinput/TunerSession.java | 17 +- .../tv/tuner/tvinput/TunerSessionWorker.java | 300 ++++--- .../tv/tuner/tvinput/TunerTvInputService.java | 30 +- src/com/android/tv/tuner/util/PostalCodeUtils.java | 89 ++ src/com/android/tv/tuner/util/StringUtils.java | 38 - .../tv/tuner/util/SystemPropertiesProxy.java | 16 + .../android/tv/tuner/util/TunerInputInfoUtils.java | 46 +- src/com/android/tv/ui/AppLayerTvView.java | 10 + src/com/android/tv/ui/ChannelBannerView.java | 137 ++-- src/com/android/tv/ui/KeypadChannelSwitchView.java | 2 +- src/com/android/tv/ui/SelectInputView.java | 73 +- src/com/android/tv/ui/TunableTvView.java | 336 +++++--- src/com/android/tv/ui/TuningBlockView.java | 113 +++ src/com/android/tv/ui/TvOverlayManager.java | 30 +- src/com/android/tv/ui/TvViewUiManager.java | 265 +----- .../tv/ui/sidepanel/ClosedCaptionFragment.java | 9 +- .../tv/ui/sidepanel/DeveloperOptionFragment.java | 23 +- src/com/android/tv/ui/sidepanel/Item.java | 12 + .../tv/ui/sidepanel/PipInputSelectorFragment.java | 170 ---- .../android/tv/ui/sidepanel/SettingsFragment.java | 20 +- src/com/android/tv/ui/sidepanel/SideFragment.java | 47 +- .../tv/ui/sidepanel/SideFragmentManager.java | 2 - .../android/tv/ui/sidepanel/SimpleActionItem.java | 34 + src/com/android/tv/ui/sidepanel/SimpleItem.java | 34 - src/com/android/tv/util/AsyncDbTask.java | 2 +- src/com/android/tv/util/Debug.java | 60 ++ src/com/android/tv/util/DurationTimer.java | 84 ++ src/com/android/tv/util/LocationUtils.java | 24 +- src/com/android/tv/util/Partner.java | 181 +++++ src/com/android/tv/util/PipInputManager.java | 432 ---------- src/com/android/tv/util/RecurringRunner.java | 9 +- src/com/android/tv/util/SearchManagerHelper.java | 61 -- src/com/android/tv/util/SetupUtils.java | 20 +- src/com/android/tv/util/StringUtils.java | 38 + src/com/android/tv/util/TvInputManagerHelper.java | 332 +++++++- src/com/android/tv/util/TvSettings.java | 148 ++-- src/com/android/tv/util/TvTrackInfoUtils.java | 37 +- src/com/android/tv/util/Utils.java | 65 +- src/com/android/tv/util/ViewCache.java | 70 ++ 276 files changed, 20620 insertions(+), 17475 deletions(-) delete mode 100644 src/com/android/tv/analytics/DurationTimer.java create mode 100644 src/com/android/tv/dialog/DvrHistoryDialogFragment.java create mode 100644 src/com/android/tv/dialog/HalfSizedDialogFragment.java delete mode 100644 src/com/android/tv/dvr/ConflictChecker.java delete mode 100644 src/com/android/tv/dvr/DvrDbSync.java delete mode 100644 src/com/android/tv/dvr/DvrPlaybackActivity.java delete mode 100644 src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java delete mode 100644 src/com/android/tv/dvr/DvrPlayer.java delete mode 100644 src/com/android/tv/dvr/DvrRecordingService.java delete mode 100644 src/com/android/tv/dvr/DvrStartRecordingReceiver.java delete mode 100644 src/com/android/tv/dvr/DvrUiHelper.java delete mode 100644 src/com/android/tv/dvr/EpisodicProgramLoadTask.java delete mode 100644 src/com/android/tv/dvr/IdGenerator.java delete mode 100644 src/com/android/tv/dvr/InputTaskScheduler.java delete mode 100644 src/com/android/tv/dvr/RecordedProgram.java delete mode 100644 src/com/android/tv/dvr/RecordingTask.java delete mode 100644 src/com/android/tv/dvr/ScheduledProgramReaper.java delete mode 100644 src/com/android/tv/dvr/ScheduledRecording.java delete mode 100644 src/com/android/tv/dvr/Scheduler.java delete mode 100644 src/com/android/tv/dvr/SeriesInfo.java delete mode 100644 src/com/android/tv/dvr/SeriesRecording.java delete mode 100644 src/com/android/tv/dvr/SeriesRecordingScheduler.java create mode 100644 src/com/android/tv/dvr/data/IdGenerator.java create mode 100644 src/com/android/tv/dvr/data/RecordedProgram.java create mode 100644 src/com/android/tv/dvr/data/ScheduledRecording.java create mode 100644 src/com/android/tv/dvr/data/SeasonEpisodeNumber.java create mode 100644 src/com/android/tv/dvr/data/SeriesInfo.java create mode 100644 src/com/android/tv/dvr/data/SeriesRecording.java create mode 100644 src/com/android/tv/dvr/provider/DvrDbSync.java create mode 100644 src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java create mode 100644 src/com/android/tv/dvr/recorder/ConflictChecker.java create mode 100644 src/com/android/tv/dvr/recorder/DvrRecordingService.java create mode 100644 src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java create mode 100644 src/com/android/tv/dvr/recorder/InputTaskScheduler.java create mode 100644 src/com/android/tv/dvr/recorder/RecordingTask.java create mode 100644 src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java create mode 100644 src/com/android/tv/dvr/recorder/Scheduler.java create mode 100644 src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java delete mode 100644 src/com/android/tv/dvr/ui/ActionPresenterSelector.java create mode 100644 src/com/android/tv/dvr/ui/BigArguments.java create mode 100644 src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java delete mode 100644 src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsContent.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsContentPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java delete mode 100644 src/com/android/tv/dvr/ui/DvrActivity.java delete mode 100644 src/com/android/tv/dvr/ui/DvrBrowseFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrDetailsActivity.java delete mode 100644 src/com/android/tv/dvr/ui/DvrDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrItemPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java delete mode 100644 src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/DvrSchedulesActivity.java create mode 100644 src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java create mode 100644 src/com/android/tv/dvr/ui/DvrUiHelper.java create mode 100644 src/com/android/tv/dvr/ui/FadeBackground.java delete mode 100644 src/com/android/tv/dvr/ui/FullScheduleCardHolder.java delete mode 100644 src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java delete mode 100644 src/com/android/tv/dvr/ui/PrioritySettingsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/RecordedProgramPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/RecordingCardView.java delete mode 100644 src/com/android/tv/dvr/ui/RecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesDeletionFragment.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java delete mode 100644 src/com/android/tv/dvr/ui/SeriesSettingsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java create mode 100644 src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsContent.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java create mode 100644 src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordingCardView.java create mode 100644 src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java create mode 100644 src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java create mode 100644 src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java create mode 100644 src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java create mode 100644 src/com/android/tv/dvr/ui/playback/DvrPlayer.java delete mode 100644 src/com/android/tv/menu/PipOptionsRowAdapter.java create mode 100644 src/com/android/tv/menu/PlaybackProgressBar.java create mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java create mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java delete mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java delete mode 100644 src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java create mode 100644 src/com/android/tv/tuner/setup/PostalCodeFragment.java create mode 100644 src/com/android/tv/tuner/util/PostalCodeUtils.java delete mode 100644 src/com/android/tv/tuner/util/StringUtils.java create mode 100644 src/com/android/tv/ui/TuningBlockView.java delete mode 100644 src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java create mode 100644 src/com/android/tv/ui/sidepanel/SimpleActionItem.java delete mode 100644 src/com/android/tv/ui/sidepanel/SimpleItem.java create mode 100644 src/com/android/tv/util/Debug.java create mode 100644 src/com/android/tv/util/DurationTimer.java create mode 100644 src/com/android/tv/util/Partner.java delete mode 100644 src/com/android/tv/util/PipInputManager.java delete mode 100644 src/com/android/tv/util/SearchManagerHelper.java create mode 100644 src/com/android/tv/util/StringUtils.java create mode 100644 src/com/android/tv/util/ViewCache.java (limited to 'src/com') 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; *

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. @@ -95,6 +129,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. */ 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 mOnTvViewChannelChangeListeners = new ArraySet<>(); + private final Set 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 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 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 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; @@ -2680,22 +2534,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC return false; } - @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(); @@ -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 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 period = Range.create(fetchStartTimeMs, endTimeMs); + Range 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 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/analytics/DurationTimer.java b/src/com/android/tv/analytics/DurationTimer.java deleted file mode 100644 index ad2d91f8..00000000 --- a/src/com/android/tv/analytics/DurationTimer.java +++ /dev/null @@ -1,62 +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.analytics; - -import android.os.SystemClock; - -/** - * Times a duration. - */ -public final class DurationTimer { - public static final long TIME_NOT_SET = -1; - - private long startTimeMs = TIME_NOT_SET; - - /** - * Returns true if the timer is running. - */ - public boolean isRunning() { - return startTimeMs != TIME_NOT_SET; - } - - /** - * Start the timer. - */ - public void start() { - startTimeMs = SystemClock.elapsedRealtime(); - } - - /** - * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long getDuration() { - return isRunning() ? SystemClock.elapsedRealtime() - startTimeMs : TIME_NOT_SET; - } - - /** - * Stops the timer and resets its value to {@link #TIME_NOT_SET}. - * - * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not - * running. - */ - public long reset() { - long duration = getDuration(); - startTimeMs = TIME_NOT_SET; - return duration; - } -} 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 @@ -51,6 +51,16 @@ public final class Channel { public static final int LOAD_IMAGE_TYPE_APP_LINK_ICON = 2; 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_NUMBER_COMPARATOR = new Comparator() { + @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,8 +97,14 @@ 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. * @@ -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; } /** @@ -278,6 +312,13 @@ public final class Channel { mLocked = locked; } + /** + * 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 @@ -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); @@ -523,6 +571,21 @@ public final class Channel { ImageLoader.loadBitmap(context, uriString, maxWidth, maxHeight, callback); } + /** + * 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 @@ -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 { + 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 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 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> { - private final Context mContext; - - public LoadChannelTask(Context context) { - mContext = context; - } - - @Override - protected List 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 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 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 { private final Context mContext; private final List 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 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 channelsToUpdate = new ArrayList<>(); + List 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 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 readTmsFile(Context context, String fileName) - throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader( - context.getAssets().open(fileName)))) { - Map 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 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 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 { - 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 { @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 { 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 { return false; } + /** + * Returns the ChannelNumber instance. + *

+ * 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 { return ret; } + /** + * Compares the channel numbers. + *

+ * 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 channels = mEpgReader.getChannels(lineupId); - for (Channel channel : channels) { - List 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 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 channels, long duration) { + List channelIds = new ArrayList<>(channels.size()); + for (Channel channel : channels) { + channelIds.add(channel.getId()); + } + Map> allPrograms = new HashMap<>(); + List 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 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 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 channels = mChannelDataManager.getChannelList(); + if (channels.isEmpty()) { + if (DEBUG) Log.d(TAG, "No existing channel to compare"); + return 0; + } + List 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 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 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 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 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 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 getLineups(@NonNull String postalCode); + List 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 getPrograms(long channelId); + Map> getPrograms(List 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 getChannels(String lineupId) { + public List getLineups(String postalCode) { return Collections.emptyList(); } @Override - public List getLineups(String postalCode) { + public List getChannelNumbers(String lineupId) { + return Collections.emptyList(); + } + + @Override + public List getChannels(String lineupId) { return Collections.emptyList(); } @@ -58,6 +64,11 @@ public class StubEpgReader implements EpgReader{ return Collections.emptyList(); } + @Override + public Map> getPrograms(List 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 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(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/dialog/HalfSizedDialogFragment.java b/src/com/android/tv/dialog/HalfSizedDialogFragment.java new file mode 100644 index 00000000..315c6a93 --- /dev/null +++ b/src/com/android/tv/dialog/HalfSizedDialogFragment.java @@ -0,0 +1,123 @@ +/* + * 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.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; + +import java.util.concurrent.TimeUnit; + +public class HalfSizedDialogFragment extends SafeDismissDialogFragment { + public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); + public static final String TRACKER_LABEL = "Half sized dialog"; + + private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); + + private OnActionClickListener mOnActionClickListener; + + private Handler mHandler = new Handler(); + private Runnable mAutoDismisser = new Runnable() { + @Override + public void run() { + dismiss(); + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.halfsized_dialog, container, false); + } + + @Override + public void onStart() { + super.onStart(); + mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + + @Override + public void onPause() { + super.onPause(); + if (mOnActionClickListener != null) { + // Dismisses the dialog to prevent the callback being forgotten during + // fragment re-creating. + dismiss(); + } + } + + @Override + public void onStop() { + super.onStop(); + mHandler.removeCallbacks(mAutoDismisser); + } + + @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; + } + + @Override + public String getTrackerLabel() { + return TRACKER_LABEL; + } + + /** + * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog + * will be automatically closed when it's paused to prevent the fragment being re-created by + * the framework, which will result the listener being forgotten. + */ + public void setOnActionClickListener(OnActionClickListener listener) { + mOnActionClickListener = listener; + } + + /** + * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. + */ + protected OnActionClickListener getOnActionClickListener() { + return mOnActionClickListener; + } + + /** + * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments + * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier + * of the action user clicked. + */ + public interface OnActionClickListener { + void onActionClick(long actionId); + } +} \ No newline at end of file diff --git a/src/com/android/tv/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; @@ -38,11 +34,6 @@ public abstract class SafeDismissDialogFragment extends DialogFragment private boolean mDismissPending = false; private Tracker mTracker; - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new TvDialog(getActivity(), getTheme()); - } - @Override public void onAttach(Activity activity) { super.onAttach(activity); @@ -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; @@ -317,6 +320,42 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { return result; } + @Override + public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) { + List 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/ConflictChecker.java b/src/com/android/tv/dvr/ConflictChecker.java deleted file mode 100644 index 201e379e..00000000 --- a/src/com/android/tv/dvr/ConflictChecker.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.annotation.TargetApi; -import android.content.ContentUris; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.Build; -import android.os.Message; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.ArraySet; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.InputSessionManager; -import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; -import com.android.tv.MainActivity; -import com.android.tv.TvApplication; -import com.android.tv.common.WeakHandler; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Checking the runtime conflict of DVR recording. - *

- * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. - */ -@TargetApi(Build.VERSION_CODES.N) -@MainThread -public class ConflictChecker { - private static final String TAG = "ConflictChecker"; - private static final boolean DEBUG = false; - - private static final int MSG_CHECK_CONFLICT = 1; - - private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30); - - /** - * To show watch conflict dialog, the start time of the earliest conflicting schedule should be - * less than or equal to this time. - */ - private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5); - /** - * To show watch conflict dialog, the start time of the earliest conflicting schedule should be - * greater than or equal to this time. - */ - private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30); - - private final MainActivity mMainActivity; - private final ChannelDataManager mChannelDataManager; - private final DvrScheduleManager mScheduleManager; - private final InputSessionManager mSessionManager; - private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this); - - private final List mUpcomingConflicts = new ArrayList<>(); - private final Set mOnUpcomingConflictChangeListeners = - new ArraySet<>(); - private final Map> mCheckedConflictsMap = new HashMap<>(); - - private final ScheduledRecordingListener mScheduledRecordingListener = - new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - }; - - private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = - new OnTvViewChannelChangeListener() { - @Override - public void onTvViewChannelChange(@Nullable Uri channelUri) { - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - } - }; - - private boolean mStarted; - - public ConflictChecker(MainActivity mainActivity) { - mMainActivity = mainActivity; - ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); - mChannelDataManager = appSingletons.getChannelDataManager(); - mScheduleManager = appSingletons.getDvrScheduleManager(); - mSessionManager = appSingletons.getInputSessionManager(); - } - - /** - * Starts checking the conflict. - */ - public void start() { - if (mStarted) { - return; - } - mStarted = true; - mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); - mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener); - mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); - } - - /** - * Stops checking the conflict. - */ - public void stop() { - if (!mStarted) { - return; - } - mStarted = false; - mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); - mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener); - mHandler.removeCallbacksAndMessages(null); - } - - /** - * Returns the upcoming conflicts. - */ - public List getUpcomingConflicts() { - return new ArrayList<>(mUpcomingConflicts); - } - - /** - * Adds a {@link OnUpcomingConflictChangeListener}. - */ - public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { - mOnUpcomingConflictChangeListeners.add(listener); - } - - /** - * Removes the {@link OnUpcomingConflictChangeListener}. - */ - public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { - mOnUpcomingConflictChangeListeners.remove(listener); - } - - private void notifyUpcomingConflictChanged() { - for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) { - l.onUpcomingConflictChange(); - } - } - - /** - * Remembers the user's decision to record while watching the channel. - */ - public void setCheckedConflictsForChannel(long mChannelId, List conflicts) { - mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); - } - - void onCheckConflict() { - // Checks the conflicting schedules and setup the next re-check time. - // If there are upcoming conflicts soon, it opens the conflict dialog. - if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); - mHandler.removeMessages(MSG_CHECK_CONFLICT); - mUpcomingConflicts.clear(); - if (!mScheduleManager.isInitialized() - || !mChannelDataManager.isDbLoadFinished()) { - mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); - notifyUpcomingConflictChanged(); - return; - } - if (mSessionManager.getCurrentTvViewChannelUri() == null) { - // As MainActivity is not using a tuner, no need to check the conflict. - notifyUpcomingConflictChanged(); - return; - } - Uri channelUri = mSessionManager.getCurrentTvViewChannelUri(); - if (TvContract.isChannelUriForPassthroughInput(channelUri)) { - notifyUpcomingConflictChanged(); - return; - } - long channelId = ContentUris.parseId(channelUri); - Channel channel = mChannelDataManager.getChannel(channelId); - // The conflicts caused by watching the channel. - List conflicts = mScheduleManager - .getConflictingSchedulesForWatching(channel.getId()); - long earliestToCheck = Long.MAX_VALUE; - long currentTimeMs = System.currentTimeMillis(); - for (ScheduledRecording schedule : conflicts) { - long startTimeMs = schedule.getStartTimeMs(); - if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) { - // The start time of the upcoming conflict remains less than the minimum - // check time. - continue; - } - if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) { - // The start time of the upcoming conflict remains greater than the - // maximum check time. Setup the next re-check time. - long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS; - if (earliestToCheck > nextCheckTimeMs) { - earliestToCheck = nextCheckTimeMs; - } - } else { - // Found upcoming conflicts which will start soon. - mUpcomingConflicts.add(schedule); - // The schedule will be removed from the "upcoming conflict" when the - // recording is almost started. - long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS; - if (earliestToCheck > nextCheckTimeMs) { - earliestToCheck = nextCheckTimeMs; - } - } - } - if (earliestToCheck != Long.MAX_VALUE) { - mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, - earliestToCheck - currentTimeMs); - } - if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); - notifyUpcomingConflictChanged(); - if (!mUpcomingConflicts.isEmpty() - && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { - // Don't show the conflict dialog if the user already knows. - List checkedConflicts = mCheckedConflictsMap.get( - channel.getId()); - if (checkedConflicts == null - || !checkedConflicts.containsAll(mUpcomingConflicts)) { - DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); - } - } - } - - private static class ConflictCheckerHandler extends WeakHandler { - ConflictCheckerHandler(ConflictChecker conflictChecker) { - super(conflictChecker); - } - - @Override - protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) { - switch (msg.what) { - case MSG_CHECK_CONFLICT: - conflictChecker.onCheckConflict(); - break; - } - } - } - - /** - * A listener for the change of upcoming conflicts. - */ - public interface OnUpcomingConflictChangeListener { - void onUpcomingConflictChange(); - } -} diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 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; @@ -210,6 +213,13 @@ public interface DvrDataManager { @NonNull Collection 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. */ 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 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 schedulesToDelete = new ArrayList<>(); List schedulesNotToDelete = new ArrayList<>(); + Set 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 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 toUpdate = new ArrayList<>(); + Set 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 movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); List movedRecordedPrograms = moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, new Filter() { @@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); + List removedSeriesRecordings = new ArrayList<>(); + List movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter() { + @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 movedSchedules = moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); List movedSeriesRecordings = moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List movedRecordedPrograms = moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, new Filter() { @@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void checkAndRemoveEmptySeriesRecording(Set 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 schedulesToDelete = new ArrayList<>(); @@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }.executeOnDbThread(); } + private void validateSeriesRecordings() { + Iterator iter = mSeriesRecordings.values().iterator(); + List 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/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java deleted file mode 100644 index df181455..00000000 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ /dev/null @@ -1,363 +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; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ContentUris; -import android.content.Context; -import android.database.ContentObserver; -import android.media.tv.TvContract.Programs; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.tv.TvApplication; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; -import com.android.tv.util.TvProviderUriMatcher; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.Queue; -import java.util.Set; - -/** - * A class to synchronizes DVR DB with TvProvider. - * - *

The current implementation of AsyncDbTask allows only one task to run at a time, and all the - * other tasks are blocked until the current one finishes. As this class performs the low priority - * jobs which take long time, it should not block others if possible. For this reason, only one - * program is queried at a time and others are queued and will be executed on the other - * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask. - */ -@MainThread -@TargetApi(Build.VERSION_CODES.N) -class DvrDbSync { - private static final String TAG = "DvrDbSync"; - private static final boolean DEBUG = false; - - private final Context mContext; - private final DvrDataManagerImpl mDataManager; - private final ChannelDataManager mChannelDataManager; - private final Queue mProgramIdQueue = new LinkedList<>(); - private QueryProgramTask mQueryProgramTask; - private final SeriesRecordingScheduler mSeriesRecordingScheduler; - private final ContentObserver mContentObserver = new ContentObserver(new Handler( - Looper.getMainLooper())) { - @SuppressLint("SwitchIntDef") - @Override - public void onChange(boolean selfChange, Uri uri) { - switch (TvProviderUriMatcher.match(uri)) { - case TvProviderUriMatcher.MATCH_PROGRAM: - if (DEBUG) Log.d(TAG, "onProgramsUpdated"); - onProgramsUpdated(); - break; - case TvProviderUriMatcher.MATCH_PROGRAM_ID: - if (DEBUG) { - Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); - } - onProgramUpdated(ContentUris.parseId(uri)); - break; - } - } - }; - - private final ChannelDataManager.Listener mChannelDataManagerListener = - new ChannelDataManager.Listener() { - @Override - public void onLoadFinished() { - start(); - } - - @Override - public void onChannelListUpdated() { - onChannelsUpdated(); - } - - @Override - public void onChannelBrowsableChanged() { } - }; - - private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - addProgramIdToCheckIfNeeded(schedule); - } - startNextUpdateIfNeeded(); - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - mProgramIdQueue.remove(schedule.getProgramId()); - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { - for (ScheduledRecording schedule : schedules) { - mProgramIdQueue.remove(schedule.getProgramId()); - addProgramIdToCheckIfNeeded(schedule); - } - startNextUpdateIfNeeded(); - } - }; - - DvrDbSync(Context context, DvrDataManagerImpl dataManager) { - this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); - } - - @VisibleForTesting - DvrDbSync(Context context, DvrDataManagerImpl dataManager, - ChannelDataManager channelDataManager) { - mContext = context; - mDataManager = dataManager; - mChannelDataManager = channelDataManager; - mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); - } - - /** - * Starts the DB sync. - */ - public void start() { - if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener(mChannelDataManagerListener); - return; - } - mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, - mContentObserver); - mDataManager.addScheduledRecordingListener(mScheduleListener); - onChannelsUpdated(); - onProgramsUpdated(); - } - - /** - * Stops the DB sync. - */ - public void stop() { - mProgramIdQueue.clear(); - if (mQueryProgramTask != null) { - mQueryProgramTask.cancel(true); - } - mChannelDataManager.removeListener(mChannelDataManagerListener); - mDataManager.removeScheduledRecordingListener(mScheduleListener); - mContext.getContentResolver().unregisterContentObserver(mContentObserver); - } - - private void onChannelsUpdated() { - List seriesRecordingsToUpdate = new ArrayList<>(); - for (SeriesRecording r : mDataManager.getSeriesRecordings()) { - if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE - && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { - seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r) - .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) - .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); - } - } - if (!seriesRecordingsToUpdate.isEmpty()) { - mDataManager.updateSeriesRecording( - SeriesRecording.toArray(seriesRecordingsToUpdate)); - } - List schedulesToRemove = new ArrayList<>(); - for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { - if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { - schedulesToRemove.add(r); - mProgramIdQueue.remove(r.getProgramId()); - } - } - if (!schedulesToRemove.isEmpty()) { - mDataManager.removeScheduledRecording( - ScheduledRecording.toArray(schedulesToRemove)); - } - } - - private void onProgramsUpdated() { - for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) { - addProgramIdToCheckIfNeeded(schedule); - } - startNextUpdateIfNeeded(); - } - - private void onProgramUpdated(long programId) { - addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId)); - startNextUpdateIfNeeded(); - } - - private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) { - if (schedule == null) { - return; - } - long programId = schedule.getProgramId(); - if (programId != ScheduledRecording.ID_NOT_SET - && !mProgramIdQueue.contains(programId) - && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); - mProgramIdQueue.offer(programId); - // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the - // schedule updates finish. - // Note that the SeriesRecordingScheduler should be paused even though the program to - // check is not episodic because it can be changed to the episodic program after the - // update, which affect the SeriesRecordingScheduler. - mSeriesRecordingScheduler.pauseUpdate(); - } - } - - private void startNextUpdateIfNeeded() { - if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) { - return; - } - if (!mProgramIdQueue.isEmpty()) { - if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek()); - mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll()); - mQueryProgramTask.executeOnDbThread(); - } else { - mSeriesRecordingScheduler.resumeUpdate(); - } - } - - @VisibleForTesting - void handleUpdateProgram(Program program, long programId) { - Set seriesRecordingsToUpdate = new HashSet<>(); - ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); - if (schedule != null - && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - if (program == null) { - mDataManager.removeScheduledRecording(schedule); - if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { - SeriesRecording seriesRecording = - mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); - if (seriesRecording != null) { - seriesRecordingsToUpdate.add(seriesRecording); - } - } - } else { - long currentTimeMs = System.currentTimeMillis(); - ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) - .setEndTimeMs(program.getEndTimeUtcMillis()) - .setSeasonNumber(program.getSeasonNumber()) - .setEpisodeNumber(program.getEpisodeNumber()) - .setEpisodeTitle(program.getEpisodeTitle()) - .setProgramDescription(program.getDescription()) - .setProgramLongDescription(program.getLongDescription()) - .setProgramPosterArtUri(program.getPosterArtUri()) - .setProgramThumbnailUri(program.getThumbnailUri()); - boolean needUpdate = false; - // Check the series recording. - SeriesRecording seriesRecordingForOldSchedule = - mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); - if (program.getSeriesId() != null) { - // New program belongs to a series. - SeriesRecording seriesRecording = - mDataManager.getSeriesRecording(program.getSeriesId()); - if (seriesRecording == null) { - // The new program is episodic while the previous one isn't. - SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext) - .getDvrManager().addSeriesRecording(program, - Collections.singletonList(program), - SeriesRecording.STATE_SERIES_STOPPED); - builder.setSeriesRecordingId(newSeriesRecording.getId()); - needUpdate = true; - } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { - // The new program belongs to the other series. - builder.setSeriesRecordingId(seriesRecording.getId()); - needUpdate = true; - seriesRecordingsToUpdate.add(seriesRecording); - if (seriesRecordingForOldSchedule != null) { - seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); - } - } else if (!Objects.equals(schedule.getSeasonNumber(), - program.getSeasonNumber()) - || !Objects.equals(schedule.getEpisodeNumber(), - program.getEpisodeNumber())) { - // The episode number has been changed. - if (seriesRecordingForOldSchedule != null) { - seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); - } - } - } else if (seriesRecordingForOldSchedule != null) { - // Old program belongs to a series but the new one doesn't. - seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); - } - // Change start time only when the recording start time has not passed. - boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs - && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); - if (needToChangeStartTime) { - builder.setStartTimeMs(program.getStartTimeUtcMillis()); - needUpdate = true; - } - if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() - || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) - || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) - || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) - || !Objects.equals(schedule.getProgramDescription(), - program.getDescription()) - || !Objects.equals(schedule.getProgramLongDescription(), - program.getLongDescription()) - || !Objects.equals(schedule.getProgramPosterArtUri(), - program.getPosterArtUri()) - || !Objects.equals(schedule.getProgramThumbnailUri(), - program.getThumbnailUri())) { - mDataManager.updateScheduledRecording(builder.build()); - } - if (!seriesRecordingsToUpdate.isEmpty()) { - // The series recordings will be updated after it's resumed. - mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); - } - } - } - } - - private class QueryProgramTask extends AsyncQueryProgramTask { - private final long mProgramId; - - QueryProgramTask(long programId) { - super(mContext.getContentResolver(), programId); - mProgramId = programId; - } - - @Override - protected void onCancelled(Program program) { - if (mQueryProgramTask == this) { - mQueryProgramTask = null; - } - startNextUpdateIfNeeded(); - } - - @Override - protected void onPostExecute(Program program) { - if (mQueryProgramTask == this) { - mQueryProgramTask = null; - } - handleUpdateProgram(program, mProgramId); - startNextUpdateIfNeeded(); - } - } -} diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 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 programsToSchedule, @SeriesState int initialState) { + List 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 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 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(); } } @@ -399,33 +411,6 @@ public class DvrManager { mDataManager.removeSeriesRecording(series); } - /** - * 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 */ @@ -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/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java deleted file mode 100644 index 5deda44a..00000000 --- a/src/com/android/tv/dvr/DvrPlaybackActivity.java +++ /dev/null @@ -1,67 +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; - -import android.app.Activity; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.util.Log; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; - -/** - * Activity to play a {@link RecordedProgram}. - */ -public class DvrPlaybackActivity extends Activity { - private static final String TAG = "DvrPlaybackActivity"; - private static final boolean DEBUG = false; - - private DvrPlaybackOverlayFragment mOverlayFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_dvr_playback); - mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() - .findFragmentById(R.id.dvr_playback_controls_fragment); - } - - @Override - public void onVisibleBehindCanceled() { - if (DEBUG) Log.d(TAG, "onVisibleBehindCanceled"); - super.onVisibleBehindCanceled(); - finish(); - } - - @Override - protected void onNewIntent(Intent intent) { - mOverlayFragment.onNewIntent(intent); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - float density = getResources().getDisplayMetrics().density; - mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), - (int) (newConfig.screenHeightDp * density)); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java deleted file mode 100644 index 9759a856..00000000 --- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java +++ /dev/null @@ -1,327 +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; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; -import android.media.session.PlaybackState; -import android.media.tv.TvContract; -import android.os.AsyncTask; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.TimeShiftUtils; -import com.android.tv.util.Utils; - -public class DvrPlaybackMediaSessionHelper { - private static final String TAG = "DvrPlaybackMediaSessionHelper"; - private static final boolean DEBUG = false; - - private int mNowPlayingCardWidth; - private int mNowPlayingCardHeight; - private int mSpeedLevel; - private long mProgramDurationMs; - - private Activity mActivity; - private DvrPlayer mDvrPlayer; - private MediaSession mMediaSession; - private final DvrWatchedPositionManager mDvrWatchedPositionManager; - private final ChannelDataManager mChannelDataManager; - - public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, - DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { - mActivity = activity; - mDvrPlayer = dvrPlayer; - mDvrWatchedPositionManager = - TvApplication.getSingletons(activity).getDvrWatchedPositionManager(); - mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager(); - mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() { - @Override - public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { - updateMediaSessionPlaybackState(); - } - - @Override - public void onPlaybackPositionChanged(long positionMs) { - updateMediaSessionPlaybackState(); - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrWatchedPositionManager - .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); - } - } - - @Override - public void onPlaybackEnded() { - // TODO: Deal with watched over recordings in DVR library - RecordedProgram nextEpisode = - overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); - if (nextEpisode == null) { - mDvrPlayer.reset(); - mActivity.finish(); - } else { - Intent intent = new Intent(activity, DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); - mActivity.startActivity(intent); - } - } - }); - initializeMediaSession(mediaSessionTag); - } - - /** - * Stops DVR player and release media session. - */ - public void release() { - if (mDvrPlayer != null) { - mDvrPlayer.reset(); - } - if (mMediaSession != null) { - mMediaSession.release(); - } - } - - /** - * Updates media session's playback state and speed. - */ - public void updateMediaSessionPlaybackState() { - mMediaSession.setPlaybackState(new PlaybackState.Builder() - .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(), - mSpeedLevel).build()); - } - - /** - * Sets the recorded program for playback. - * - * @param program The recorded program to play. {@code null} to reset the DVR player. - */ - public void setupPlayback(RecordedProgram program, long seekPositionMs) { - if (program != null) { - mDvrPlayer.setProgram(program, seekPositionMs); - setupMediaSession(program); - } else { - mDvrPlayer.reset(); - mMediaSession.setActive(false); - } - } - - /** - * Returns the recorded program now playing. - */ - public RecordedProgram getProgram() { - return mDvrPlayer.getProgram(); - } - - /** - * Checks if the recorded program is the same as now playing one. - */ - public boolean isCurrentProgram(RecordedProgram program) { - return program != null && program.equals(getProgram()); - } - - /** - * Returns playback state. - */ - public int getPlaybackState() { - return mDvrPlayer.getPlaybackState(); - } - - /** - * Returns the underlying DVR player. - */ - public DvrPlayer getDvrPlayer() { - return mDvrPlayer; - } - - private void initializeMediaSession(String mediaSessionTag) { - mMediaSession = new MediaSession(mActivity, mediaSessionTag); - mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); - mNowPlayingCardWidth = mActivity.getResources() - .getDimensionPixelSize(R.dimen.notif_card_img_max_width); - mNowPlayingCardHeight = mActivity.getResources() - .getDimensionPixelSize(R.dimen.notif_card_img_height); - mMediaSession.setCallback(new MediaSessionCallback()); - mActivity.setMediaController( - new MediaController(mActivity, mMediaSession.getSessionToken())); - updateMediaSessionPlaybackState(); - } - - private void setupMediaSession(RecordedProgram program) { - mProgramDurationMs = program.getDurationMillis(); - String cardTitleText = program.getTitle(); - if (TextUtils.isEmpty(cardTitleText)) { - Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - cardTitleText = (channel != null) ? channel.getDisplayName() - : mActivity.getString(R.string.no_program_information); - } - updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(), - mProgramDurationMs, null, 0); - String posterArtUri = program.getPosterArtUri(); - if (posterArtUri == null) { - posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); - } - updatePosterArt(program, cardTitleText, program.getDescription(), - mProgramDurationMs, null, posterArtUri); - mMediaSession.setActive(true); - } - - private void updatePosterArt(RecordedProgram program, String cardTitleText, - String cardSubtitleText, long duration, - @Nullable Bitmap posterArt, @Nullable String posterArtUri) { - if (posterArt != null) { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, posterArt, 0); - } else if (posterArtUri != null) { - ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, - mNowPlayingCardHeight, new ProgramPosterArtCallback( - mActivity, program, cardTitleText, cardSubtitleText, duration)); - } else { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, null, R.drawable.default_now_card); - } - } - - private class ProgramPosterArtCallback extends - ImageLoader.ImageLoaderCallback { - private RecordedProgram mRecordedProgram; - private String mCardTitleText; - private String mCardSubtitleText; - private long mDuration; - - public ProgramPosterArtCallback(Activity activity, RecordedProgram program, - String cardTitleText, String cardSubtitleText, long duration) { - super(activity); - mRecordedProgram = program; - mCardTitleText = cardTitleText; - mCardSubtitleText = cardSubtitleText; - mDuration = duration; - } - - @Override - public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { - if (isCurrentProgram(mRecordedProgram)) { - updatePosterArt(mRecordedProgram, mCardTitleText, - mCardSubtitleText, mDuration, posterArt, null); - } - } - } - - private void updateMediaMetadata(final long programId, final String title, - final String subtitle, final long duration, - final Bitmap posterArt, final int imageResId) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... arg0) { - MediaMetadata.Builder builder = new MediaMetadata.Builder(); - builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId) - .putString(MediaMetadata.METADATA_KEY_TITLE, title) - .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); - if (subtitle != null) { - builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); - } - Bitmap programPosterArt = posterArt; - if (programPosterArt == null && imageResId != 0) { - programPosterArt = - BitmapFactory.decodeResource(mActivity.getResources(), imageResId); - } - if (programPosterArt != null) { - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); - } - mMediaSession.setMetadata(builder.build()); - return null; - } - }.execute(); - } - - // An event was triggered by MediaController.TransportControls and must be handled here. - // Here we update the media itself to act on the event that was triggered. - private class MediaSessionCallback extends MediaSession.Callback { - @Override - public void onPrepare() { - if (!mDvrPlayer.isPlaybackPrepared()) { - mDvrPlayer.prepare(true); - } - } - - @Override - public void onPlay() { - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrPlayer.play(); - } - } - - @Override - public void onPause() { - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrPlayer.pause(); - } - } - - @Override - public void onFastForward() { - if (!mDvrPlayer.isPlaybackPrepared()) { - return; - } - if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { - if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { - mSpeedLevel++; - } else { - return; - } - } else { - mSpeedLevel = 0; - } - mDvrPlayer.fastForward( - TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); - } - - @Override - public void onRewind() { - if (!mDvrPlayer.isPlaybackPrepared()) { - return; - } - if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { - if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { - mSpeedLevel++; - } else { - return; - } - } else { - mSpeedLevel = 0; - } - mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); - } - - @Override - public void onSeekTo(long positionMs) { - if (mDvrPlayer.isPlaybackPrepared()) { - mDvrPlayer.seekTo(positionMs); - } - } - } -} diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java deleted file mode 100644 index 5656655c..00000000 --- a/src/com/android/tv/dvr/DvrPlayer.java +++ /dev/null @@ -1,425 +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; - -import android.media.PlaybackParams; -import android.media.tv.TvContentRating; -import android.media.tv.TvInputManager; -import android.media.tv.TvTrackInfo; -import android.media.tv.TvView; -import android.media.session.PlaybackState; -import android.util.Log; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class DvrPlayer { - private static final String TAG = "DvrPlayer"; - private static final boolean DEBUG = false; - - /** - * The max rewinding speed supported by DVR player. - */ - public static final int MAX_REWIND_SPEED = 256; - /** - * The max fast-forwarding speed supported by DVR player. - */ - public static final int MAX_FAST_FORWARD_SPEED = 256; - - private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); - private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 - - private RecordedProgram mProgram; - private long mInitialSeekPositionMs; - private final TvView mTvView; - private DvrPlayerCallback mCallback; - private AspectRatioChangedListener mAspectRatioChangedListener; - private ContentBlockedListener mContentBlockedListener; - private float mAspectRatio = Float.NaN; - private int mPlaybackState = PlaybackState.STATE_NONE; - private long mTimeShiftCurrentPositionMs; - private boolean mPauseOnPrepared; - private final PlaybackParams mPlaybackParams = new PlaybackParams(); - private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); - private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - private boolean mTimeShiftPlayAvailable; - - public static class DvrPlayerCallback { - /** - * Called when the playback position is changed. The normal updating frequency is - * around 1 sec., which is restricted to the implementation of - * {@link android.media.tv.TvInputService}. - */ - public void onPlaybackPositionChanged(long positionMs) { } - /** - * Called when the playback state or the playback speed is changed. - */ - public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } - /** - * Called when the playback toward the end. - */ - public void onPlaybackEnded() { } - } - - public interface AspectRatioChangedListener { - /** - * Called when the Video's aspect ratio is changed. - */ - void onAspectRatioChanged(float videoAspectRatio); - } - - public interface ContentBlockedListener { - /** - * Called when the Video's aspect ratio is changed. - */ - void onContentBlocked(TvContentRating rating); - } - - public DvrPlayer(TvView tvView) { - mTvView = tvView; - mPlaybackParams.setSpeed(1.0f); - setTvViewCallbacks(); - setCallback(null); - } - - /** - * Prepares playback. - * - * @param doPlay indicates DVR player do or do not start playback after media is prepared. - */ - public void prepare(boolean doPlay) throws IllegalStateException { - if (DEBUG) Log.d(TAG, "prepare()"); - if (mProgram == null) { - throw new IllegalStateException("Recorded program not set"); - } else if (mPlaybackState != PlaybackState.STATE_NONE) { - throw new IllegalStateException("Playback is already prepared"); - } - mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); - mPlaybackState = PlaybackState.STATE_CONNECTING; - mPauseOnPrepared = !doPlay; - mCallback.onPlaybackStateChanged(mPlaybackState, 1); - } - - /** - * Resumes playback. - */ - public void play() throws IllegalStateException { - if (DEBUG) Log.d(TAG, "play()"); - if (!isPlaybackPrepared()) { - throw new IllegalStateException("Recorded program not set or video not ready yet"); - } - switch (mPlaybackState) { - case PlaybackState.STATE_FAST_FORWARDING: - case PlaybackState.STATE_REWINDING: - setPlaybackSpeed(1); - break; - default: - mTvView.timeShiftResume(); - } - mPlaybackState = PlaybackState.STATE_PLAYING; - mCallback.onPlaybackStateChanged(mPlaybackState, 1); - } - - /** - * Pauses playback. - */ - public void pause() throws IllegalStateException { - if (DEBUG) Log.d(TAG, "pause()"); - if (!isPlaybackPrepared()) { - throw new IllegalStateException("Recorded program not set or playback not started yet"); - } - switch (mPlaybackState) { - case PlaybackState.STATE_FAST_FORWARDING: - case PlaybackState.STATE_REWINDING: - setPlaybackSpeed(1); - // falls through - case PlaybackState.STATE_PLAYING: - mTvView.timeShiftPause(); - mPlaybackState = PlaybackState.STATE_PAUSED; - break; - default: - break; - } - mCallback.onPlaybackStateChanged(mPlaybackState, 1); - } - - /** - * Fast-forwards playback with the given speed. If the given speed is larger than - * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. - */ - public void fastForward(int speed) throws IllegalStateException { - if (DEBUG) Log.d(TAG, "fastForward()"); - if (!isPlaybackPrepared()) { - throw new IllegalStateException("Recorded program not set or playback not started yet"); - } - if (speed <= 0) { - throw new IllegalArgumentException("Speed cannot be negative or 0"); - } - if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { - return; - } - speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); - if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); - setPlaybackSpeed(speed); - mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; - mCallback.onPlaybackStateChanged(mPlaybackState, speed); - } - - /** - * Rewinds playback with the given speed. If the given speed is larger than - * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. - */ - public void rewind(int speed) throws IllegalStateException { - if (DEBUG) Log.d(TAG, "rewind()"); - if (!isPlaybackPrepared()) { - throw new IllegalStateException("Recorded program not set or playback not started yet"); - } - if (speed <= 0) { - throw new IllegalArgumentException("Speed cannot be negative or 0"); - } - if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { - return; - } - speed = Math.min(speed, MAX_REWIND_SPEED); - if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); - setPlaybackSpeed(-speed); - mPlaybackState = PlaybackState.STATE_REWINDING; - mCallback.onPlaybackStateChanged(mPlaybackState, speed); - } - - /** - * Seeks playback to the specified position. - */ - public void seekTo(long positionMs) throws IllegalStateException { - if (DEBUG) Log.d(TAG, "seekTo()"); - if (!isPlaybackPrepared()) { - throw new IllegalStateException("Recorded program not set or playback not started yet"); - } - if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { - return; - } - positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); - if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); - mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); - if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || - mPlaybackState == PlaybackState.STATE_REWINDING) { - mPlaybackState = PlaybackState.STATE_PLAYING; - mTvView.timeShiftResume(); - mCallback.onPlaybackStateChanged(mPlaybackState, 1); - } - } - - /** - * Resets playback. - */ - public void reset() { - if (DEBUG) Log.d(TAG, "reset()"); - mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); - mPlaybackState = PlaybackState.STATE_NONE; - mTvView.reset(); - mTimeShiftPlayAvailable = false; - mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - mTimeShiftCurrentPositionMs = 0; - mPlaybackParams.setSpeed(1.0f); - mProgram = null; - } - - /** - * Sets callbacks for playback. - */ - public void setCallback(DvrPlayerCallback callback) { - if (callback != null) { - mCallback = callback; - } else { - mCallback = mEmptyCallback; - } - } - - /** - * Sets listener to aspect ratio changing. - */ - public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { - mAspectRatioChangedListener = listener; - } - - /** - * Sets listener to content blocking. - */ - public void setContentBlockedListener(ContentBlockedListener listener) { - mContentBlockedListener = listener; - } - - /** - * Sets recorded programs for playback. If the player is playing another program, stops it. - */ - public void setProgram(RecordedProgram program, long initialSeekPositionMs) { - if (mProgram != null && mProgram.equals(program)) { - return; - } - if (mPlaybackState != PlaybackState.STATE_NONE) { - reset(); - } - mInitialSeekPositionMs = initialSeekPositionMs; - mProgram = program; - } - - /** - * Returns the recorded program now playing. - */ - public RecordedProgram getProgram() { - return mProgram; - } - - /** - * Returns the currrent playback posistion in msecs. - */ - public long getPlaybackPosition() { - return mTimeShiftCurrentPositionMs; - } - - /** - * Returns the playback speed currently used. - */ - public int getPlaybackSpeed() { - return (int) mPlaybackParams.getSpeed(); - } - - /** - * Returns the playback state defined in {@link android.media.session.PlaybackState}. - */ - public int getPlaybackState() { - return mPlaybackState; - } - - /** - * Returns if playback of the recorded program is started. - */ - public boolean isPlaybackPrepared() { - return mPlaybackState != PlaybackState.STATE_NONE - && mPlaybackState != PlaybackState.STATE_CONNECTING; - } - - private void setPlaybackSpeed(int speed) { - mPlaybackParams.setSpeed(speed); - mTvView.timeShiftSetPlaybackParams(mPlaybackParams); - } - - private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { - return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); - } - - private void setTvViewCallbacks() { - mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { - @Override - public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { - if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); - mStartPositionMs = timeMs; - if (mTimeShiftPlayAvailable) { - resumeToWatchedPositionIfNeeded(); - } - } - - @Override - public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { - if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); - if (!mTimeShiftPlayAvailable) { - // Workaround of b/31436263 - return; - } - // Workaround of b/32211561, TIF won't report start position when TIS report - // its start position as 0. In that case, we have to do the prework of playback - // on the first time we get current position, and the start position should be 0 - // at that time. - if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mStartPositionMs = 0; - resumeToWatchedPositionIfNeeded(); - } - timeMs -= mStartPositionMs; - if (mPlaybackState == PlaybackState.STATE_REWINDING - && timeMs <= REWIND_POSITION_MARGIN_MS) { - play(); - } else { - mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); - mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); - if (timeMs >= mProgram.getDurationMillis()) { - pause(); - mCallback.onPlaybackEnded(); - } - } - } - }); - mTvView.setCallback(new TvView.TvInputCallback() { - @Override - public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); - if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE - && mPlaybackState == PlaybackState.STATE_CONNECTING) { - mTimeShiftPlayAvailable = true; - } - } - - @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (trackId == null || type != TvTrackInfo.TYPE_VIDEO - || mAspectRatioChangedListener == null) { - return; - } - List trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); - if (trackInfos != null) { - for (TvTrackInfo trackInfo : trackInfos) { - if (trackInfo.getId().equals(trackId)) { - float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() - * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); - if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); - if (!Float.isNaN(videoAspectRatio) - && mAspectRatio != videoAspectRatio) { - mAspectRatioChangedListener - .onAspectRatioChanged(videoAspectRatio); - mAspectRatio = videoAspectRatio; - return; - } - } - } - } - } - - @Override - public void onContentBlocked(String inputId, TvContentRating rating) { - if (mContentBlockedListener != null) { - mContentBlockedListener.onContentBlocked(rating); - } - } - }); - } - - private void resumeToWatchedPositionIfNeeded() { - if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, - SEEK_POSITION_MARGIN_MS) + mStartPositionMs); - mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - } - if (mPauseOnPrepared) { - mTvView.timeShiftPause(); - mPlaybackState = PlaybackState.STATE_PAUSED; - mPauseOnPrepared = false; - } else { - mTvView.timeShiftResume(); - mPlaybackState = PlaybackState.STATE_PLAYING; - } - mCallback.onPlaybackStateChanged(mPlaybackState, 1); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java deleted file mode 100644 index 8c40aaa8..00000000 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import android.app.AlarmManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.HandlerThread; -import android.os.IBinder; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.util.Clock; -import com.android.tv.util.RecurringRunner; - -/** - * DVR Scheduler service. - * - *

This service is responsible for: - *

- * - *

The service does not stop it self. - */ -public class DvrRecordingService extends Service { - private static final String TAG = "DvrRecordingService"; - private static final boolean DEBUG = false; - public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler"; - - public static void startService(Context context) { - Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); - context.startService(dvrSchedulerIntent); - } - - private final Clock mClock = Clock.SYSTEM; - private RecurringRunner mReaperRunner; - - private Scheduler mScheduler; - private HandlerThread mHandlerThread; - - @Override - public void onCreate() { - TvApplication.setCurrentRunningProcess(this, true); - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(); - SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); - ApplicationSingletons singletons = TvApplication.getSingletons(this); - WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); - - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - // mScheduler may have been set for testing. - if (mScheduler == null) { - mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); - mHandlerThread.start(); - mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(), - singletons.getInputSessionManager(), dataManager, - singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), this, - mClock, alarmManager); - mScheduler.start(); - } - mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), - new ScheduledProgramReaper(dataManager, mClock), null); - mReaperRunner.start(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); - mScheduler.update(); - return START_STICKY; - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mReaperRunner.stop(); - mScheduler.stop(); - mScheduler = null; - if (mHandlerThread != null) { - mHandlerThread.quitSafely(); - mHandlerThread = null; - } - super.onDestroy(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @VisibleForTesting - void setScheduler(Scheduler scheduler) { - Log.i(TAG, "Setting scheduler for tests to " + scheduler); - mScheduler = scheduler; - } -} 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> 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> mInputConflictInfoMap = - new HashMap<>(); + // although there's conflict, it might still be recorded partially. + private final Map> mInputConflictInfoMap = new HashMap<>(); private boolean mInitialized; @@ -171,10 +172,9 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } - Map conflictInfo = - mInputConflictInfoMap.get(inputId); + Map 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 conflictInfo = - mInputConflictInfoMap.get(inputId); + Map 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 addedConflicts = new ArrayList<>(); List removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - Map oldConflictsInfo = mInputConflictInfoMap.get(inputId); + Map oldConflictInfo = mInputConflictInfoMap.get(inputId); Map 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 conflictInfo = getConflictingSchedulesInfo(inputId); - if (conflictInfo.isEmpty()) { + List conflicts = getConflictingSchedulesInfo(inputId); + if (conflicts.isEmpty()) { mInputConflictInfoMap.remove(inputId); } else { - mInputConflictInfoMap.put(inputId, conflictInfo); - List conflicts = new ArrayList<>(conflictInfo.keySet()); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map 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. *

* 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 schedulesForSeries = mDataManager.getScheduledRecordings( + List scheduledRecordingForSeries = mDataManager.getScheduledRecordings( seriesRecording.getId()); - return getConflictingSchedules(input, schedulesForSeries); + List 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 getConflictingSchedulesInfo(String inputId) { + private List 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 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 conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.containsKey(schedule); + Map 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 conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.getOrDefault(schedule, false); + Map 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 getConflictingSchedules( List schedules, int tunerCount, List> periods) { - List result = new ArrayList<>( - getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); - Collections.sort(result, RESULT_COMPARATOR); + List result = new ArrayList<>(); + for (ConflictInfo conflictInfo : + getConflictingSchedulesInfo(schedules, tunerCount, periods)) { + result.add(conflictInfo.schedule); + } return result; } @VisibleForTesting - static Map getConflictingSchedulesInfo( - List schedules, int tunerCount) { + static List getConflictingSchedulesInfo(List 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 getConflictingSchedulesInfo( + private static List getConflictingSchedulesInfo( List schedules, int tunerCount, List> periods) { List schedulesToCheck = new ArrayList<>(schedules); // Sort by the same order as that in InputTaskScheduler. Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); List recordings = new ArrayList<>(); - Map conflicts = new HashMap<>(); + Map conflicts = new HashMap<>(); Map 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 result = new ArrayList<>(conflicts.values()); + Collections.sort(result, new Comparator() { + @Override + public int compare(ConflictInfo lhs, ConflictInfo rhs) { + return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); + } + }); + return result; } private static void removeFinishedRecordings(List 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. + *

+ * 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/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/DvrStartRecordingReceiver.java deleted file mode 100644 index 6d2f0d43..00000000 --- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import com.android.tv.TvApplication; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Signals the DVR to start recording shows soon. - */ -public class DvrStartRecordingReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - TvApplication.setCurrentRunningProcess(context, true); - DvrRecordingService.startService(context); - } -} diff --git a/src/com/android/tv/dvr/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 { + private class CleanUpDbTask extends AsyncTask { 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 ops = getDeleteOps(storageStatus - == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL); + if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + return true; + } + List 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 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 getDeleteOps(boolean deleteAll) { + private List getDeleteOps() { List 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/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java deleted file mode 100644 index c0d3b0c5..00000000 --- a/src/com/android/tv/dvr/DvrUiHelper.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.media.tv.TvInputManager; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityOptionsCompat; -import android.text.TextUtils; -import android.widget.ImageView; -import android.widget.Toast; - -import com.android.tv.MainActivity; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; -import com.android.tv.data.Program; -import com.android.tv.dvr.ui.DvrDetailsActivity; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrSchedulesActivity; -import com.android.tv.dvr.ui.DvrSeriesDeletionActivity; -import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity; -import com.android.tv.dvr.ui.DvrSeriesSettingsActivity; -import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; -import com.android.tv.dvr.ui.list.DvrSchedulesFragment; -import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; -import com.android.tv.util.Utils; - -import java.util.Collections; -import java.util.List; - -/** - * A helper class for DVR UI. - */ -@MainThread -@TargetApi(Build.VERSION_CODES.N) -public class DvrUiHelper { - /** - * Handles the action to create the new schedule. It returns {@code true} if the schedule is - * added and there's no additional UI, otherwise {@code false}. - */ - public static boolean handleCreateSchedule(MainActivity activity, Program program) { - if (program == null) { - return false; - } - DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); - if (!program.isEpisodic()) { - // One time recording. - dvrManager.addSchedule(program); - if (!dvrManager.getConflictingSchedules(program).isEmpty()) { - DvrUiHelper.showScheduleConflictDialog(activity, program); - return false; - } - } else { - SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null || 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; - - } - - /** - * Checks if the storage status is good for recording and shows error messages if needed. - * - * @return true if the storage status is fine to be recorded for {@code inputId}. - */ - public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) { - if (!Utils.isBundledInput(inputId)) { - return true; - } - DvrStorageStatusManager dvrStorageStatusManager = - TvApplication.getSingletons(activity).getDvrStorageStatusManager(); - int status = dvrStorageStatusManager.getDvrStorageStatus(); - if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { - showDvrSmallSizedStorageErrorDialog(activity); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { - showDvrMissingStorageErrorDialog(activity, inputId); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) { - // TODO: handle insufficient storage case. - return true; - } else { - return true; - } - } - - /** - * Shows the schedule dialog. - */ - public static void showScheduleDialog(MainActivity activity, Program program) { - if (SoftPreconditions.checkNotNull(program) == null) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); - } - - /** - * Shows the recording duration options dialog. - */ - public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) { - if (SoftPreconditions.checkNotNull(channel) == null) { - return; - } - Bundle args = new Bundle(); - args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); - showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args); - } - - /** - * Shows the dialog which says that the new schedule conflicts with others. - */ - public static void showScheduleConflictDialog(MainActivity activity, Program program) { - if (program == null) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true); - } - - /** - * Shows the conflict dialog for the channel watching. - */ - public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) { - if (channel == null) { - return; - } - Bundle args = new Bundle(); - args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); - showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args); - } - - /** - * Shows DVR insufficient space error dialog. - */ - public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) { - showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null); - Utils.clearRecordingFailedReason(activity, - TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - } - - /** - * Shows DVR missing storage error dialog. - */ - 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); - } - - /** - * Shows DVR small sized storage error dialog. - */ - public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { - showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); - } - - /** - * Shows stop recording dialog. - */ - public static void showStopRecordingDialog(Activity activity, long channelId, int reason, - HalfSizedDialogFragment.OnActionClickListener listener) { - Bundle args = new Bundle(); - args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); - args.putInt(DvrStopRecordingFragment.KEY_REASON, reason); - DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment(); - fragment.setOnActionClickListener(listener); - showDialogFragment(activity, fragment, args); - } - - /** - * Shows "already scheduled" dialog. - */ - public static void showAlreadyScheduleDialog(MainActivity activity, Program program) { - if (program == null) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true); - } - - /** - * Shows "already recorded" dialog. - */ - public static void showAlreadyRecordedDialog(MainActivity activity, Program program) { - if (program == null) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); - } - - private static void showDialogFragment(Activity activity, - DvrHalfSizedDialogFragment dialogFragment, Bundle args) { - showDialogFragment(activity, dialogFragment, args, false, false); - } - - private static void showDialogFragment(Activity activity, - DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory, - boolean keepProgramGuide) { - dialogFragment.setArguments(args); - if (activity instanceof MainActivity) { - ((MainActivity) activity).getOverlayManager() - .showDialogFragment(DvrHalfSizedDialogFragment.DIALOG_TAG, dialogFragment, - keepSidePanelHistory, keepProgramGuide); - } else { - dialogFragment.show(activity.getFragmentManager(), - DvrHalfSizedDialogFragment.DIALOG_TAG); - } - } - - /** - * Checks whether channel watch conflict dialog is open or not. - */ - public static boolean isChannelWatchConflictDialogShown(MainActivity activity) { - return activity.getOverlayManager().getCurrentDialog() instanceof - DvrChannelWatchConflictDialogFragment; - } - - private static ScheduledRecording getEarliestScheduledRecording(List - recordings) { - ScheduledRecording earlistScheduledRecording = null; - if (!recordings.isEmpty()) { - Collections.sort(recordings, - ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); - earlistScheduledRecording = recordings.get(0); - } - return earlistScheduledRecording; - } - - /** - * Shows the schedules activity to resolve the tune conflict. - */ - public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) { - if (channel == null) { - return; - } - List conflicts = TvApplication.getSingletons(context).getDvrManager() - .getConflictingSchedulesForTune(channel.getId()); - startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); - } - - /** - * Shows the schedules activity to resolve the one time recording conflict. - */ - public static void startSchedulesActivityForOneTimeRecordingConflict(Context context, - List conflicts) { - startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); - } - - /** - * Shows the schedules activity with full schedule. - */ - public static void startSchedulesActivity(Context context, ScheduledRecording - focusedScheduledRecording) { - Intent intent = new Intent(context, DvrSchedulesActivity.class); - intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, - DvrSchedulesActivity.TYPE_FULL_SCHEDULE); - if (focusedScheduledRecording != null) { - intent.putExtra(DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING, - focusedScheduledRecording); - } - context.startActivity(intent); - } - - /** - * Shows the schedules activity for series recording. - */ - public static void startSchedulesActivityForSeries(Context context, - SeriesRecording seriesRecording) { - Intent intent = new Intent(context, DvrSchedulesActivity.class); - intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, - DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); - intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, - seriesRecording); - context.startActivity(intent); - } - - /** - * Shows the series settings activity. - * - * @param channelIds Channel ID list which has programs belonging to the series. - */ - public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, - @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { - Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); - intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); - intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); - intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, - removeEmptySeriesSchedule); - intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); - intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, - showViewScheduleOptionInDialog); - context.startActivity(intent); - } - - /** - * Shows "series recording scheduled" dialog activity. - */ - public static void StartSeriesScheduledDialogActivity(Context context, - SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { - if (seriesRecording == null) { - return; - } - Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); - intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, - seriesRecording.getId()); - intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, - showViewScheduleOptionInDialog); - context.startActivity(intent); - } - - /** - * Shows the details activity for the DVR items. The type of DVR items may be - * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. - */ - public static void startDetailsActivity(Activity activity, Object dvrItem, - @Nullable ImageView imageView, boolean hideViewSchedule) { - if (dvrItem == null) { - return; - } - Intent intent = new Intent(activity, DvrDetailsActivity.class); - long recordingId; - int viewType; - if (dvrItem instanceof ScheduledRecording) { - ScheduledRecording schedule = (ScheduledRecording) dvrItem; - recordingId = schedule.getId(); - if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; - } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; - } else { - return; - } - } else if (dvrItem instanceof RecordedProgram) { - recordingId = ((RecordedProgram) dvrItem).getId(); - viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; - } else if (dvrItem instanceof SeriesRecording) { - recordingId = ((SeriesRecording) dvrItem).getId(); - viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; - } else { - return; - } - intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); - intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); - Bundle bundle = null; - if (imageView != null) { - bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); - } - activity.startActivity(intent, bundle); - } - - /** - * Shows the cancel all dialog for series schedules list. - */ - public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, - SeriesRecording seriesRecording) { - DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = - new DvrStopSeriesRecordingDialogFragment(); - Bundle arguments = new Bundle(); - arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, - seriesRecording); - dvrStopSeriesRecordingDialogFragment.setArguments(arguments); - dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), - DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); - } - - /** - * Shows the series deletion activity. - */ - public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) { - Intent intent = new Intent(context, DvrSeriesDeletionActivity.class); - intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId); - context.startActivity(intent); - } - - public static void showAddScheduleToast(Context context, - String title, long startTimeMs, long endTimeMs) { - String msg = (startTimeMs > System.currentTimeMillis()) ? - context.getString(R.string.dvr_msg_program_scheduled, title) - : context.getString(R.string.dvr_msg_current_program_scheduled, title, - Utils.toTimeString(endTimeMs, false)); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); - } -} diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java 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/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java deleted file mode 100644 index 15ca2700..00000000 --- a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java +++ /dev/null @@ -1,382 +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; - -import android.annotation.TargetApi; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvContract.Programs; -import android.net.Uri; -import android.os.Build; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; - -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Program; -import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; -import com.android.tv.util.AsyncDbTask.CursorFilter; -import com.android.tv.util.PermissionUtils; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -/** - * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. - */ -@TargetApi(Build.VERSION_CODES.N) -abstract public class EpisodicProgramLoadTask { - private static final String TAG = "EpisodicProgramLoadTask"; - - private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); - private static final int START_TIME_INDEX = - Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS); - private static final int RECORDING_PROHIBITED_INDEX = - Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); - - private static final String PARAM_START_TIME = "start_time"; - private static final String PARAM_END_TIME = "end_time"; - - private static final String PROGRAM_PREDICATE = - Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND " - + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; - private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = - Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND " - + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; - private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; - private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; - - private final Context mContext; - private final DvrDataManager mDataManager; - private boolean mQueryAllChannels; - private boolean mLoadCurrentProgram; - private boolean mLoadScheduledEpisode; - private boolean mLoadDisallowedProgram; - // If true, match programs with OPTION_CHANNEL_ALL. - private boolean mIgnoreChannelOption; - private final ArrayList mSeriesRecordings = new ArrayList<>(); - private AsyncProgramQueryTask mProgramQueryTask; - - /** - * - * Constructor used to load programs for one series recording with the given channel option. - */ - public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { - this(context, Collections.singletonList(seriesRecording)); - } - - /** - * Constructor used to load programs for multiple series recordings. The channel option is - * {@link SeriesRecording#OPTION_CHANNEL_ALL}. - */ - public EpisodicProgramLoadTask(Context context, Collection seriesRecordings) { - mContext = context.getApplicationContext(); - mDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mSeriesRecordings.addAll(seriesRecordings); - } - - /** - * Returns the series recordings. - */ - public List getSeriesRecordings() { - return mSeriesRecordings; - } - - /** - * Returns the program query task. It is {@code null} until it is executed. - */ - @Nullable - public AsyncProgramQueryTask getTask() { - return mProgramQueryTask; - } - - /** - * Enables loading current programs. The default value is {@code false}. - */ - public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); - mLoadCurrentProgram = loadCurrentProgram; - return this; - } - - /** - * Enables already schedules episodes. The default value is {@code false}. - */ - public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); - mLoadScheduledEpisode = loadScheduledEpisode; - return this; - } - - /** - * Enables loading disallowed programs whose schedules were removed manually by the user. - * The default value is {@code false}. - */ - public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); - mLoadDisallowedProgram = loadDisallowedProgram; - return this; - } - - /** - * Gives the option whether to ignore the channel option when matching programs. - * If {@code ignoreChannelOption} is {@code true}, the program will be matched with - * {@link SeriesRecording#OPTION_CHANNEL_ALL} option. - */ - public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { - SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't change setting after execution."); - mIgnoreChannelOption = ignoreChannelOption; - return this; - } - - /** - * Executes the task. - * - * @see com.android.tv.util.AsyncDbTask#executeOnDbThread - */ - public void execute() { - if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG, - "Can't execute task: the task is already running.")) { - mQueryAllChannels = mSeriesRecordings.size() > 1 - || mSeriesRecordings.get(0).getChannelOption() - == SeriesRecording.OPTION_CHANNEL_ALL - || mIgnoreChannelOption; - mProgramQueryTask = createTask(); - mProgramQueryTask.executeOnDbThread(); - } - } - - /** - * Cancels the task. - * - * @see android.os.AsyncTask#cancel - */ - public void cancel(boolean mayInterruptIfRunning) { - if (mProgramQueryTask != null) { - mProgramQueryTask.cancel(mayInterruptIfRunning); - } - } - - /** - * Runs on the UI thread after the program loading finishes successfully. - */ - protected void onPostExecute(List programs) { - } - - /** - * Runs on the UI thread after the program loading was canceled. - */ - protected void onCancelled(List programs) { - } - - private AsyncProgramQueryTask createTask() { - SqlParams sqlParams = createSqlParams(); - return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, - sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) { - @Override - protected void onPostExecute(List programs) { - EpisodicProgramLoadTask.this.onPostExecute(programs); - } - - @Override - protected void onCancelled(List programs) { - EpisodicProgramLoadTask.this.onCancelled(programs); - } - }; - } - - private SqlParams createSqlParams() { - SqlParams sqlParams = new SqlParams(); - if (PermissionUtils.hasAccessAllEpg(mContext)) { - sqlParams.uri = Programs.CONTENT_URI; - // Base - StringBuilder selection = new StringBuilder(mLoadCurrentProgram - ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE); - List args = new ArrayList<>(); - args.add(Long.toString(System.currentTimeMillis())); - // Channel option - if (!mQueryAllChannels) { - selection.append(" AND ").append(CHANNEL_ID_PREDICATE); - args.add(Long.toString(mSeriesRecordings.get(0).getChannelId())); - } - // Title - if (mSeriesRecordings.size() == 1) { - selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE); - args.add(mSeriesRecordings.get(0).getTitle()); - } - sqlParams.selection = selection.toString(); - sqlParams.selectionArgs = args.toArray(new String[args.size()]); - sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings); - } else { - // The query includes the current program. Will be filtered if needed. - if (mQueryAllChannels) { - sqlParams.uri = Programs.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_START_TIME, - String.valueOf(System.currentTimeMillis())) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) - .build(); - } else { - sqlParams.uri = TvContract.buildProgramsUriForChannel( - mSeriesRecordings.get(0).getChannelId(), - System.currentTimeMillis(), Long.MAX_VALUE); - } - sqlParams.selection = null; - sqlParams.selectionArgs = null; - sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings); - } - return sqlParams; - } - - @VisibleForTesting - static boolean isEpisodeScheduled(Collection 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 mDisallowedProgramIds = new HashSet<>(); - private final Set mScheduledEpisodes = new HashSet<>(); - - SeriesRecordingCursorFilter(List seriesRecordings) { - if (!mLoadDisallowedProgram) { - mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); - } - if (!mLoadScheduledEpisode) { - Set seriesRecordingIds = new HashSet<>(); - for (SeriesRecording r : seriesRecordings) { - seriesRecordingIds.add(r.getId()); - } - for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { - if (seriesRecordingIds.contains(r.getSeriesRecordingId()) - && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED - && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mScheduledEpisodes.add(new ScheduledEpisode(r)); - } - } - } - } - - @Override - @WorkerThread - public boolean filter(Cursor c) { - if (!mLoadDisallowedProgram - && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { - return false; - } - Program program = Program.fromCursor(c); - for (SeriesRecording seriesRecording : mSeriesRecordings) { - boolean programMatches; - if (mIgnoreChannelOption) { - programMatches = seriesRecording.matchProgram(program, - SeriesRecording.OPTION_CHANNEL_ALL); - } else { - programMatches = seriesRecording.matchProgram(program); - } - if (programMatches) { - return mLoadScheduledEpisode - || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber())); - } - } - return false; - } - } - - private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { - SeriesRecordingCursorFilterForNonSystem(List seriesRecordings) { - super(seriesRecordings); - } - - @Override - public boolean filter(Cursor c) { - return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) - && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); - } - } - - private static class SqlParams { - public Uri uri; - public String selection; - public String[] selectionArgs; - public CursorFilter filter; - } - - /** - * A plain java object which includes the season/episode number for the series recording. - */ - public static class ScheduledEpisode { - public final long seriesRecordingId; - public final String seasonNumber; - public final String episodeNumber; - - /** - * Create a new Builder with the values set from an existing {@link ScheduledRecording}. - */ - ScheduledEpisode(ScheduledRecording r) { - this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); - } - - public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { - this.seriesRecordingId = seriesRecordingId; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ScheduledEpisode)) return false; - ScheduledEpisode that = (ScheduledEpisode) o; - return seriesRecordingId == that.seriesRecordingId - && Objects.equals(seasonNumber, that.seasonNumber) - && Objects.equals(episodeNumber, that.episodeNumber); - } - - @Override - public int hashCode() { - return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); - } - - @Override - public String toString() { - return "ScheduledEpisode{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; - } - } -} diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/IdGenerator.java deleted file mode 100644 index 0ed6362c..00000000 --- a/src/com/android/tv/dvr/IdGenerator.java +++ /dev/null @@ -1,50 +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; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * A class which generate the ID which increases sequentially. - */ -public class IdGenerator { - /** - * ID generator for the scheduled recording. - */ - public static final IdGenerator SCHEDULED_RECORDING = new IdGenerator(); - - /** - * ID generator for the series recording. - */ - public static final IdGenerator SERIES_RECORDING = new IdGenerator(); - - private final AtomicLong mMaxId = new AtomicLong(0); - - /** - * Sets the new maximum ID. - */ - public void setMaxId(long maxId) { - mMaxId.set(maxId); - } - - /** - * Returns the new ID which is greater than the existing maximum ID by 1. - */ - public long newId() { - return mMaxId.incrementAndGet(); - } -} diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java deleted file mode 100644 index 53c89ebc..00000000 --- a/src/com/android/tv/dvr/InputTaskScheduler.java +++ /dev/null @@ -1,431 +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; - -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.ArrayMap; -import android.util.Log; -import android.util.LongSparseArray; - -import com.android.tv.InputSessionManager; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.util.Clock; -import com.android.tv.util.CompositeComparator; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * The scheduler for a TV input. - */ -public class InputTaskScheduler { - private static final String TAG = "InputTaskScheduler"; - private static final boolean DEBUG = false; - - private static final int MSG_ADD_SCHEDULED_RECORDING = 1; - private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2; - private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3; - private static final int MSG_BUILD_SCHEDULE = 4; - private static final int MSG_STOP_SCHEDULE = 5; - - private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f; - - // The candidate comparator should be the consistent with - // DvrScheduleManager#CANDIDATE_COMPARATOR. - private static final Comparator CANDIDATE_COMPARATOR = - new CompositeComparator<>( - RecordingTask.PRIORITY_COMPARATOR, - RecordingTask.END_TIME_COMPARATOR, - RecordingTask.ID_COMPARATOR); - - /** - * Returns the comparator which the schedules are sorted with when executed. - */ - public static Comparator getRecordingOrderComparator() { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; - } - - /** - * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. - */ - public final class HandlerWrapper extends Handler { - public static final int MESSAGE_REMOVE = 999; - private final long mId; - private final RecordingTask mTask; - - HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, - RecordingTask recordingTask) { - super(looper, recordingTask); - mId = scheduledRecording.getId(); - mTask = recordingTask; - mTask.setHandler(this); - } - - @Override - public void handleMessage(Message msg) { - // The RecordingTask gets a chance first. - // It must return false to pass this message to here. - if (msg.what == MESSAGE_REMOVE) { - if (DEBUG) Log.d(TAG, "done " + mId); - mPendingRecordings.remove(mId); - } - removeCallbacksAndMessages(null); - mHandler.removeMessages(MSG_BUILD_SCHEDULE); - mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); - super.handleMessage(msg); - } - } - - private TvInputInfo mInput; - private final Looper mLooper; - private final ChannelDataManager mChannelDataManager; - private final DvrManager mDvrManager; - private final WritableDvrDataManager mDataManager; - private final InputSessionManager mSessionManager; - private final Clock mClock; - private final Context mContext; - - private final LongSparseArray mPendingRecordings = new LongSparseArray<>(); - private final Map mWaitingSchedules = new ArrayMap<>(); - private final Handler mMainThreadHandler; - private final Handler mHandler; - private final Object mInputLock = new Object(); - private final RecordingTaskFactory mRecordingTaskFactory; - - public InputTaskScheduler(Context context, TvInputInfo input, Looper looper, - ChannelDataManager channelDataManager, DvrManager dvrManager, - DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) { - this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager, - clock, new Handler(Looper.getMainLooper()), null, null); - } - - @VisibleForTesting - InputTaskScheduler(Context context, TvInputInfo input, Looper looper, - ChannelDataManager channelDataManager, DvrManager dvrManager, - DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, - Handler mainThreadHandler, @Nullable Handler workerThreadHandler, - RecordingTaskFactory recordingTaskFactory) { - if (DEBUG) Log.d(TAG, "Creating scheduler for " + input); - mContext = context; - mInput = input; - mLooper = looper; - mChannelDataManager = channelDataManager; - mDvrManager = dvrManager; - mDataManager = (WritableDvrDataManager) dataManager; - mSessionManager = sessionManager; - mClock = clock; - mMainThreadHandler = mainThreadHandler; - mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory - : new RecordingTaskFactory() { - @Override - public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock) { - return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager, - mDataManager, mClock); - } - }; - if (workerThreadHandler == null) { - mHandler = new WorkerThreadHandler(looper); - } else { - mHandler = workerThreadHandler; - } - } - - /** - * Adds a {@link ScheduledRecording}. - */ - public void addSchedule(ScheduledRecording schedule) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); - } - - @VisibleForTesting - void handleAddSchedule(ScheduledRecording schedule) { - if (mPendingRecordings.get(schedule.getId()) != null - || mWaitingSchedules.containsKey(schedule.getId())) { - return; - } - mWaitingSchedules.put(schedule.getId(), schedule); - mHandler.removeMessages(MSG_BUILD_SCHEDULE); - mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); - } - - /** - * Removes the {@link ScheduledRecording}. - */ - public void removeSchedule(ScheduledRecording schedule) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); - } - - @VisibleForTesting - void handleRemoveSchedule(ScheduledRecording schedule) { - HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); - if (wrapper != null) { - wrapper.mTask.cancel(); - return; - } - if (mWaitingSchedules.containsKey(schedule.getId())) { - mWaitingSchedules.remove(schedule.getId()); - mHandler.removeMessages(MSG_BUILD_SCHEDULE); - mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); - } - } - - /** - * Updates the {@link ScheduledRecording}. - */ - public void updateSchedule(ScheduledRecording schedule) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); - } - - @VisibleForTesting - void handleUpdateSchedule(ScheduledRecording schedule) { - HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); - if (wrapper != null) { - if (schedule.getStartTimeMs() > mClock.currentTimeMillis() - && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) { - // It shouldn't have started. Cancel and put to the waiting list. - // The schedules will be rebuilt when the task is removed. - // The reschedule is called in Scheduler. - wrapper.mTask.cancel(); - mWaitingSchedules.put(schedule.getId(), schedule); - return; - } - wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule)); - return; - } - if (mWaitingSchedules.containsKey(schedule.getId())) { - mWaitingSchedules.put(schedule.getId(), schedule); - mHandler.removeMessages(MSG_BUILD_SCHEDULE); - mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); - } - } - - /** - * Updates the TV input. - */ - public void updateTvInputInfo(TvInputInfo input) { - synchronized (mInputLock) { - mInput = input; - } - } - - /** - * Stops the input task scheduler. - */ - public void stop() { - mHandler.removeCallbacksAndMessages(null); - mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); - } - - private void handleStopSchedule() { - mWaitingSchedules.clear(); - int size = mPendingRecordings.size(); - for (int i = 0; i < size; ++i) { - RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; - task.cleanUp(); - } - } - - @VisibleForTesting - void handleBuildSchedule() { - if (mWaitingSchedules.isEmpty()) { - return; - } - long currentTimeMs = mClock.currentTimeMillis(); - // Remove past schedules. - for (Iterator iter = mWaitingSchedules.values().iterator(); - iter.hasNext(); ) { - ScheduledRecording schedule = iter.next(); - if (schedule.getEndTimeMs() - currentTimeMs - <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { - fail(schedule); - iter.remove(); - } - } - if (mWaitingSchedules.isEmpty()) { - return; - } - // Record the schedules which should start now. - List schedulesToStart = new ArrayList<>(); - for (ScheduledRecording schedule : mWaitingSchedules.values()) { - if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED - && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) { - schedulesToStart.add(schedule); - } - } - // The schedules will be executed with the following order. - // 1. The schedule which starts early. It can be replaced later when the schedule with the - // higher priority needs to start. - // 2. The schedule with the higher priority. It can be replaced later when the schedule with - // the higher priority needs to start. - // 3. The schedule which was created recently. - Collections.sort(schedulesToStart, getRecordingOrderComparator()); - int tunerCount; - synchronized (mInputLock) { - tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; - } - for (ScheduledRecording schedule : schedulesToStart) { - if (hasTaskWhichFinishEarlier(schedule)) { - // If there is a schedule which finishes earlier than the new schedule, rebuild the - // schedules after it finishes. - return; - } - if (mPendingRecordings.size() < tunerCount) { - // Tuners available. - createRecordingTask(schedule).start(); - mWaitingSchedules.remove(schedule.getId()); - } else { - // No available tuners. - RecordingTask task = getReplacableTask(schedule); - if (task != null) { - task.stop(); - // Just return. The schedules will be rebuilt after the task is stopped. - return; - } - } - } - if (mWaitingSchedules.isEmpty()) { - return; - } - // Set next scheduling. - long earliest = Long.MAX_VALUE; - for (ScheduledRecording schedule : mWaitingSchedules.values()) { - // The conflicting schedules will be removed if they end before conflicting resolved. - if (schedulesToStart.contains(schedule)) { - if (earliest > schedule.getEndTimeMs()) { - earliest = schedule.getEndTimeMs(); - } - } else { - if (earliest > schedule.getStartTimeMs() - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { - earliest = schedule.getStartTimeMs() - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; - } - } - } - mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); - } - - private RecordingTask createRecordingTask(ScheduledRecording schedule) { - Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); - RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel, - mDvrManager, mSessionManager, mDataManager, mClock); - HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask); - mPendingRecordings.put(schedule.getId(), handlerWrapper); - return recordingTask; - } - - private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) { - int size = mPendingRecordings.size(); - for (int i = 0; i < size; ++i) { - RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; - if (task.getEndTimeMs() <= schedule.getStartTimeMs()) { - return true; - } - } - return false; - } - - private RecordingTask getReplacableTask(ScheduledRecording schedule) { - // Returns the recording with the following priority. - // 1. The recording with the lowest priority is returned. - // 2. If the priorities are the same, the recording which finishes early is returned. - // 3. If 1) and 2) are the same, the early created schedule is returned. - int size = mPendingRecordings.size(); - RecordingTask candidate = null; - for (int i = 0; i < size; ++i) { - RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; - if (schedule.getPriority() > task.getPriority()) { - if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { - candidate = task; - } - } - } - return candidate; - } - - private void fail(ScheduledRecording schedule) { - // It's called when the scheduling has been failed without creating RecordingTask. - runOnMainHandler(new Runnable() { - @Override - public void run() { - ScheduledRecording scheduleInManager = - mDataManager.getScheduledRecording(schedule.getId()); - if (scheduleInManager != null) { - // The schedule should be updated based on the object from DataManager in case - // when it has been updated. - mDataManager.changeState(scheduleInManager, - ScheduledRecording.STATE_RECORDING_FAILED); - } - } - }); - } - - private void runOnMainHandler(Runnable runnable) { - if (Looper.myLooper() == mMainThreadHandler.getLooper()) { - runnable.run(); - } else { - mMainThreadHandler.post(runnable); - } - } - - @VisibleForTesting - interface RecordingTaskFactory { - RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock); - } - - private class WorkerThreadHandler extends Handler { - public WorkerThreadHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_ADD_SCHEDULED_RECORDING: - handleAddSchedule((ScheduledRecording) msg.obj); - break; - case MSG_REMOVE_SCHEDULED_RECORDING: - handleRemoveSchedule((ScheduledRecording) msg.obj); - break; - case MSG_UPDATE_SCHEDULED_RECORDING: - handleUpdateSchedule((ScheduledRecording) msg.obj); - case MSG_BUILD_SCHEDULE: - handleBuildSchedule(); - break; - case MSG_STOP_SCHEDULE: - handleStopSchedule(); - break; - } - } - } -} diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java deleted file mode 100644 index dd744f80..00000000 --- a/src/com/android/tv/dvr/RecordedProgram.java +++ /dev/null @@ -1,868 +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; - -import static android.media.tv.TvContract.RecordedPrograms; - -import android.annotation.TargetApi; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.Build; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.tv.common.R; -import com.android.tv.data.BaseProgram; -import com.android.tv.data.GenreItems; -import com.android.tv.data.InternalDataUtils; -import com.android.tv.util.Utils; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -/** - * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. - */ -@TargetApi(Build.VERSION_CODES.N) -public class RecordedProgram extends BaseProgram { - public static final int ID_NOT_SET = -1; - - public final static String[] PROJECTION = { - // These are in exactly the order listed in RecordedPrograms - RecordedPrograms._ID, - RecordedPrograms.COLUMN_PACKAGE_NAME, - RecordedPrograms.COLUMN_INPUT_ID, - RecordedPrograms.COLUMN_CHANNEL_ID, - RecordedPrograms.COLUMN_TITLE, - RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, - RecordedPrograms.COLUMN_SEASON_TITLE, - RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, - RecordedPrograms.COLUMN_EPISODE_TITLE, - RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_BROADCAST_GENRE, - RecordedPrograms.COLUMN_CANONICAL_GENRE, - RecordedPrograms.COLUMN_SHORT_DESCRIPTION, - RecordedPrograms.COLUMN_LONG_DESCRIPTION, - RecordedPrograms.COLUMN_VIDEO_WIDTH, - RecordedPrograms.COLUMN_VIDEO_HEIGHT, - RecordedPrograms.COLUMN_AUDIO_LANGUAGE, - RecordedPrograms.COLUMN_CONTENT_RATING, - RecordedPrograms.COLUMN_POSTER_ART_URI, - RecordedPrograms.COLUMN_THUMBNAIL_URI, - RecordedPrograms.COLUMN_SEARCHABLE, - RecordedPrograms.COLUMN_RECORDING_DATA_URI, - RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, - RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, - RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, - RecordedPrograms.COLUMN_VERSION_NUMBER, - RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, - }; - - public static RecordedProgram fromCursor(Cursor cursor) { - int index = 0; - Builder builder = builder() - .setId(cursor.getLong(index++)) - .setPackageName(cursor.getString(index++)) - .setInputId(cursor.getString(index++)) - .setChannelId(cursor.getLong(index++)) - .setTitle(cursor.getString(index++)) - .setSeasonNumber(cursor.getString(index++)) - .setSeasonTitle(cursor.getString(index++)) - .setEpisodeNumber(cursor.getString(index++)) - .setEpisodeTitle(cursor.getString(index++)) - .setStartTimeUtcMillis(cursor.getLong(index++)) - .setEndTimeUtcMillis(cursor.getLong(index++)) - .setBroadcastGenres(cursor.getString(index++)) - .setCanonicalGenres(cursor.getString(index++)) - .setShortDescription(cursor.getString(index++)) - .setLongDescription(cursor.getString(index++)) - .setVideoWidth(cursor.getInt(index++)) - .setVideoHeight(cursor.getInt(index++)) - .setAudioLanguage(cursor.getString(index++)) - .setContentRating(cursor.getString(index++)) - .setPosterArtUri(cursor.getString(index++)) - .setThumbnailUri(cursor.getString(index++)) - .setSearchable(cursor.getInt(index++) == 1) - .setDataUri(cursor.getString(index++)) - .setDataBytes(cursor.getLong(index++)) - .setDurationMillis(cursor.getLong(index++)) - .setExpireTimeUtcMillis(cursor.getLong(index++)) - .setInternalProviderFlag1(cursor.getInt(index++)) - .setInternalProviderFlag2(cursor.getInt(index++)) - .setInternalProviderFlag3(cursor.getInt(index++)) - .setInternalProviderFlag4(cursor.getInt(index++)) - .setVersionNumber(cursor.getInt(index++)); - if (Utils.isInBundledPackageSet(builder.mPackageName)) { - InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); - } - return builder.build(); - } - - public static ContentValues toValues(RecordedProgram recordedProgram) { - ContentValues values = new ContentValues(); - if (recordedProgram.mId != ID_NOT_SET) { - values.put(RecordedPrograms._ID, recordedProgram.mId); - } - values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId); - values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId); - values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle); - values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber); - values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle); - values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber); - values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle); - values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, - recordedProgram.mStartTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, - safeEncode(recordedProgram.mBroadcastGenres)); - values.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, - safeEncode(recordedProgram.mCanonicalGenres)); - values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription); - values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription); - if (recordedProgram.mVideoWidth == 0) { - values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH); - } else { - values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth); - } - if (recordedProgram.mVideoHeight == 0) { - values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT); - } else { - values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); - } - values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); - values.put(RecordedPrograms.COLUMN_CONTENT_RATING, recordedProgram.mContentRating); - values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri); - values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri); - values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0); - values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, - safeToString(recordedProgram.mDataUri)); - values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes); - values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, - recordedProgram.mDurationMillis); - values.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, - recordedProgram.mExpireTimeUtcMillis); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, - InternalDataUtils.serializeInternalProviderData(recordedProgram)); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, - recordedProgram.mInternalProviderFlag1); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, - recordedProgram.mInternalProviderFlag2); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, - recordedProgram.mInternalProviderFlag3); - values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, - recordedProgram.mInternalProviderFlag4); - values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber); - return values; - } - - public static class Builder{ - private long mId = ID_NOT_SET; - private String mPackageName; - private String mInputId; - private long mChannelId; - private String mTitle; - private String mSeriesId; - private String mSeasonNumber; - private String mSeasonTitle; - private String mEpisodeNumber; - private String mEpisodeTitle; - private long mStartTimeUtcMillis; - private long mEndTimeUtcMillis; - private String[] mBroadcastGenres; - private String[] mCanonicalGenres; - private String mShortDescription; - private String mLongDescription; - private int mVideoWidth; - private int mVideoHeight; - private String mAudioLanguage; - private String mContentRating; - private String mPosterArtUri; - private String mThumbnailUri; - private boolean mSearchable = true; - private Uri mDataUri; - private long mDataBytes; - private long mDurationMillis; - private long mExpireTimeUtcMillis; - private int mInternalProviderFlag1; - private int mInternalProviderFlag2; - private int mInternalProviderFlag3; - private int mInternalProviderFlag4; - private int mVersionNumber; - - public Builder setId(long id) { - mId = id; - return this; - } - - public Builder setPackageName(String packageName) { - mPackageName = packageName; - return this; - } - - public Builder setInputId(String inputId) { - mInputId = inputId; - return this; - } - - public Builder setChannelId(long channelId) { - mChannelId = channelId; - return this; - } - - public Builder setTitle(String title) { - mTitle = title; - return this; - } - - public Builder setSeriesId(String seriesId) { - mSeriesId = seriesId; - return this; - } - - public Builder setSeasonNumber(String seasonNumber) { - mSeasonNumber = seasonNumber; - return this; - } - - public Builder setSeasonTitle(String seasonTitle) { - mSeasonTitle = seasonTitle; - return this; - } - - public Builder setEpisodeNumber(String episodeNumber) { - mEpisodeNumber = episodeNumber; - return this; - } - - public Builder setEpisodeTitle(String episodeTitle) { - mEpisodeTitle = episodeTitle; - return this; - } - - public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { - mStartTimeUtcMillis = startTimeUtcMillis; - return this; - } - - public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { - mEndTimeUtcMillis = endTimeUtcMillis; - return this; - } - - public Builder setBroadcastGenres(String broadcastGenres) { - if (TextUtils.isEmpty(broadcastGenres)) { - mBroadcastGenres = null; - return this; - } - return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres)); - } - - private Builder setBroadcastGenres(String[] broadcastGenres) { - mBroadcastGenres = broadcastGenres; - return this; - } - - public Builder setCanonicalGenres(String canonicalGenres) { - if (TextUtils.isEmpty(canonicalGenres)) { - mCanonicalGenres = null; - return this; - } - return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres)); - } - - private Builder setCanonicalGenres(String[] canonicalGenres) { - mCanonicalGenres = canonicalGenres; - return this; - } - - public Builder setShortDescription(String shortDescription) { - mShortDescription = shortDescription; - return this; - } - - public Builder setLongDescription(String longDescription) { - mLongDescription = longDescription; - return this; - } - - public Builder setVideoWidth(int videoWidth) { - mVideoWidth = videoWidth; - return this; - } - - public Builder setVideoHeight(int videoHeight) { - mVideoHeight = videoHeight; - return this; - } - - public Builder setAudioLanguage(String audioLanguage) { - mAudioLanguage = audioLanguage; - return this; - } - - public Builder setContentRating(String contentRating) { - mContentRating = contentRating; - return this; - } - - private Uri toUri(String uriString) { - try { - return uriString == null ? null : Uri.parse(uriString); - } catch (Exception e) { - return null; - } - } - - public Builder setPosterArtUri(String posterArtUri) { - mPosterArtUri = posterArtUri; - return this; - } - - public Builder setThumbnailUri(String thumbnailUri) { - mThumbnailUri = thumbnailUri; - return this; - } - - public Builder setSearchable(boolean searchable) { - mSearchable = searchable; - return this; - } - - public Builder setDataUri(String dataUri) { - return setDataUri(toUri(dataUri)); - } - - public Builder setDataUri(Uri dataUri) { - mDataUri = dataUri; - return this; - } - - public Builder setDataBytes(long dataBytes) { - mDataBytes = dataBytes; - return this; - } - - public Builder setDurationMillis(long durationMillis) { - mDurationMillis = durationMillis; - return this; - } - - public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) { - mExpireTimeUtcMillis = expireTimeUtcMillis; - return this; - } - - public Builder setInternalProviderFlag1(int internalProviderFlag1) { - mInternalProviderFlag1 = internalProviderFlag1; - return this; - } - - public Builder setInternalProviderFlag2(int internalProviderFlag2) { - mInternalProviderFlag2 = internalProviderFlag2; - return this; - } - - public Builder setInternalProviderFlag3(int internalProviderFlag3) { - mInternalProviderFlag3 = internalProviderFlag3; - return this; - } - - public Builder setInternalProviderFlag4(int internalProviderFlag4) { - mInternalProviderFlag4 = internalProviderFlag4; - return this; - } - - public Builder setVersionNumber(int versionNumber) { - mVersionNumber = versionNumber; - return this; - } - - public RecordedProgram build() { - // Generate the series ID for the episodic program of other TV input. - if (TextUtils.isEmpty(mSeriesId) - && !TextUtils.isEmpty(mEpisodeNumber)) { - setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle)); - } - return new RecordedProgram(mId, mPackageName, mInputId, mChannelId, mTitle, mSeriesId, - mSeasonNumber, mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis, - mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription, - mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRating, - mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes, - mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1, - mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4, - mVersionNumber); - } - } - - public static Builder builder() { return new Builder(); } - - public static Builder buildFrom(RecordedProgram orig) { - return builder() - .setId(orig.getId()) - .setPackageName(orig.getPackageName()) - .setInputId(orig.getInputId()) - .setChannelId(orig.getChannelId()) - .setTitle(orig.getTitle()) - .setSeriesId(orig.getSeriesId()) - .setSeasonNumber(orig.getSeasonNumber()) - .setSeasonTitle(orig.getSeasonTitle()) - .setEpisodeNumber(orig.getEpisodeNumber()) - .setEpisodeTitle(orig.getEpisodeTitle()) - .setStartTimeUtcMillis(orig.getStartTimeUtcMillis()) - .setEndTimeUtcMillis(orig.getEndTimeUtcMillis()) - .setBroadcastGenres(orig.getBroadcastGenres()) - .setCanonicalGenres(orig.getCanonicalGenres()) - .setShortDescription(orig.getDescription()) - .setLongDescription(orig.getLongDescription()) - .setVideoWidth(orig.getVideoWidth()) - .setVideoHeight(orig.getVideoHeight()) - .setAudioLanguage(orig.getAudioLanguage()) - .setContentRating(orig.getContentRating()) - .setPosterArtUri(orig.getPosterArtUri()) - .setThumbnailUri(orig.getThumbnailUri()) - .setSearchable(orig.isSearchable()) - .setInternalProviderFlag1(orig.getInternalProviderFlag1()) - .setInternalProviderFlag2(orig.getInternalProviderFlag2()) - .setInternalProviderFlag3(orig.getInternalProviderFlag3()) - .setInternalProviderFlag4(orig.getInternalProviderFlag4()) - .setVersionNumber(orig.getVersionNumber()); - } - - public static final Comparator START_TIME_THEN_ID_COMPARATOR = - new Comparator() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - int res = - Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis()); - if (res != 0) { - return res; - } - return Long.compare(lhs.mId, rhs.mId); - } - }; - - private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); - - private final long mId; - private final String mPackageName; - private final String mInputId; - private final long mChannelId; - private final String mTitle; - private final String mSeriesId; - private final String mSeasonNumber; - private final String mSeasonTitle; - private final String mEpisodeNumber; - private final String mEpisodeTitle; - private final long mStartTimeUtcMillis; - private final long mEndTimeUtcMillis; - private final String[] mBroadcastGenres; - private final String[] mCanonicalGenres; - private final String mShortDescription; - private final String mLongDescription; - private final int mVideoWidth; - private final int mVideoHeight; - private final String mAudioLanguage; - private final String mContentRating; - private final String mPosterArtUri; - private final String mThumbnailUri; - private final boolean mSearchable; - private final Uri mDataUri; - private final long mDataBytes; - private final long mDurationMillis; - private final long mExpireTimeUtcMillis; - private final int mInternalProviderFlag1; - private final int mInternalProviderFlag2; - private final int mInternalProviderFlag3; - private final int mInternalProviderFlag4; - private final int mVersionNumber; - - private RecordedProgram(long id, String packageName, String inputId, long channelId, - String title, String seriesId, String seasonNumber, String seasonTitle, - String episodeNumber, String episodeTitle, long startTimeUtcMillis, - long endTimeUtcMillis, String[] broadcastGenres, String[] canonicalGenres, - String shortDescription, String longDescription, int videoWidth, int videoHeight, - String audioLanguage, String contentRating, String posterArtUri, String thumbnailUri, - boolean searchable, Uri dataUri, long dataBytes, long durationMillis, - long expireTimeUtcMillis, int internalProviderFlag1, int internalProviderFlag2, - int internalProviderFlag3, int internalProviderFlag4, int versionNumber) { - mId = id; - mPackageName = packageName; - mInputId = inputId; - mChannelId = channelId; - mTitle = title; - mSeriesId = seriesId; - mSeasonNumber = seasonNumber; - mSeasonTitle = seasonTitle; - mEpisodeNumber = episodeNumber; - mEpisodeTitle = episodeTitle; - mStartTimeUtcMillis = startTimeUtcMillis; - mEndTimeUtcMillis = endTimeUtcMillis; - mBroadcastGenres = broadcastGenres; - mCanonicalGenres = canonicalGenres; - mShortDescription = shortDescription; - mLongDescription = longDescription; - mVideoWidth = videoWidth; - mVideoHeight = videoHeight; - - mAudioLanguage = audioLanguage; - mContentRating = contentRating; - mPosterArtUri = posterArtUri; - mThumbnailUri = thumbnailUri; - mSearchable = searchable; - mDataUri = dataUri; - mDataBytes = dataBytes; - mDurationMillis = durationMillis; - mExpireTimeUtcMillis = expireTimeUtcMillis; - mInternalProviderFlag1 = internalProviderFlag1; - mInternalProviderFlag2 = internalProviderFlag2; - mInternalProviderFlag3 = internalProviderFlag3; - mInternalProviderFlag4 = internalProviderFlag4; - mVersionNumber = versionNumber; - } - - public String getAudioLanguage() { - return mAudioLanguage; - } - - public String[] getBroadcastGenres() { - return mBroadcastGenres; - } - - public String[] getCanonicalGenres() { - return mCanonicalGenres; - } - - /** - * Returns array of canonical genre ID's for this recorded program. - */ - @Override - public int[] getCanonicalGenreIds() { - if (mCanonicalGenres == null) { - return null; - } - int[] genreIds = new int[mCanonicalGenres.length]; - for (int i = 0; i < mCanonicalGenres.length; i++) { - genreIds[i] = GenreItems.getId(mCanonicalGenres[i]); - } - return genreIds; - } - - @Override - public long getChannelId() { - return mChannelId; - } - - public String getContentRating() { - return mContentRating; - } - - public Uri getDataUri() { - return mDataUri; - } - - public long getDataBytes() { - return mDataBytes; - } - - @Override - public long getDurationMillis() { - return mDurationMillis; - } - - @Override - public long getEndTimeUtcMillis() { - return mEndTimeUtcMillis; - } - - @Override - public String getEpisodeNumber() { - return mEpisodeNumber; - } - - public String getEpisodeTitle() { - return mEpisodeTitle; - } - - @Override - public String getEpisodeDisplayTitle(Context context) { - if (!TextUtils.isEmpty(mEpisodeNumber)) { - String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; - if (TextUtils.equals(mSeasonNumber, "0")) { - // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_title_format_no_season_number), - mEpisodeNumber, episodeTitle); - } else { - return String.format(context.getResources().getString( - R.string.display_episode_title_format), - mSeasonNumber, mEpisodeNumber, episodeTitle); - } - } - return mEpisodeTitle; - } - - @Nullable - @Override - public String getTitleWithEpisodeNumber(Context context) { - if (TextUtils.isEmpty(mTitle)) { - return mTitle; - } - if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { - return TextUtils.isEmpty(mEpisodeNumber) ? mTitle : context.getString( - R.string.program_title_with_episode_number_no_season, mTitle, mEpisodeNumber); - } else { - return context.getString(R.string.program_title_with_episode_number, mTitle, - mSeasonNumber, mEpisodeNumber); - } - } - - @Nullable - public String getEpisodeDisplayNumber(Context context) { - if (!TextUtils.isEmpty(mEpisodeNumber)) { - if (TextUtils.equals(mSeasonNumber, "0")) { - // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_number_format_no_season_number), mEpisodeNumber); - } else { - return String.format(context.getResources().getString( - R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber); - } - } - return null; - } - - public long getExpireTimeUtcMillis() { - return mExpireTimeUtcMillis; - } - - public long getId() { - return mId; - } - - public String getPackageName() { - return mPackageName; - } - - public String getInputId() { - return mInputId; - } - - public int getInternalProviderFlag1() { - return mInternalProviderFlag1; - } - - public int getInternalProviderFlag2() { - return mInternalProviderFlag2; - } - - public int getInternalProviderFlag3() { - return mInternalProviderFlag3; - } - - public int getInternalProviderFlag4() { - return mInternalProviderFlag4; - } - - @Override - public String getDescription() { - return mShortDescription; - } - - @Override - public String getLongDescription() { - return mLongDescription; - } - - @Override - public String getPosterArtUri() { - return mPosterArtUri; - } - - @Override - public boolean isValid() { - return true; - } - - public boolean isSearchable() { - return mSearchable; - } - - @Override - public String getSeriesId() { - return mSeriesId; - } - - @Override - public String getSeasonNumber() { - return mSeasonNumber; - } - - public String getSeasonTitle() { - return mSeasonTitle; - } - - @Override - public long getStartTimeUtcMillis() { - return mStartTimeUtcMillis; - } - - @Override - public String getThumbnailUri() { - return mThumbnailUri; - } - - @Override - public String getTitle() { - return mTitle; - } - - public Uri getUri() { - return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId); - } - - public int getVersionNumber() { - return mVersionNumber; - } - - public int getVideoHeight() { - return mVideoHeight; - } - - public int getVideoWidth() { - return mVideoWidth; - } - - /** - * Checks whether the recording has been clipped or not. - */ - public boolean isClipped() { - return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RecordedProgram that = (RecordedProgram) o; - return Objects.equals(mId, that.mId) && - Objects.equals(mChannelId, that.mChannelId) && - Objects.equals(mSeriesId, that.mSeriesId) && - Objects.equals(mSeasonNumber, that.mSeasonNumber) && - Objects.equals(mSeasonTitle, that.mSeasonTitle) && - Objects.equals(mEpisodeNumber, that.mEpisodeNumber) && - Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) && - Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) && - Objects.equals(mVideoWidth, that.mVideoWidth) && - Objects.equals(mVideoHeight, that.mVideoHeight) && - Objects.equals(mSearchable, that.mSearchable) && - Objects.equals(mDataBytes, that.mDataBytes) && - Objects.equals(mDurationMillis, that.mDurationMillis) && - Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) && - Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) && - Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) && - Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) && - Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) && - Objects.equals(mVersionNumber, that.mVersionNumber) && - Objects.equals(mTitle, that.mTitle) && - Objects.equals(mEpisodeTitle, that.mEpisodeTitle) && - Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) && - Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) && - Objects.equals(mShortDescription, that.mShortDescription) && - Objects.equals(mLongDescription, that.mLongDescription) && - Objects.equals(mAudioLanguage, that.mAudioLanguage) && - Objects.equals(mContentRating, that.mContentRating) && - Objects.equals(mPosterArtUri, that.mPosterArtUri) && - Objects.equals(mThumbnailUri, that.mThumbnailUri); - } - - /** - * Hashes based on the ID. - */ - @Override - public int hashCode() { - return Objects.hash(mId); - } - - @Override - public String toString() { - return "RecordedProgram" - + "[" + mId + - "]{ mPackageName=" + mPackageName + - ", mInputId='" + mInputId + '\'' + - ", mChannelId='" + mChannelId + '\'' + - ", mTitle='" + mTitle + '\'' + - ", mSeriesId='" + mSeriesId + '\'' + - ", mEpisodeNumber=" + mEpisodeNumber + - ", mEpisodeTitle='" + mEpisodeTitle + '\'' + - ", mStartTimeUtcMillis=" + mStartTimeUtcMillis + - ", mEndTimeUtcMillis=" + mEndTimeUtcMillis + - ", mBroadcastGenres=" + - (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") + - ", mCanonicalGenres=" + - (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") + - ", mShortDescription='" + mShortDescription + '\'' + - ", mLongDescription='" + mLongDescription + '\'' + - ", mVideoHeight=" + mVideoHeight + - ", mVideoWidth=" + mVideoWidth + - ", mAudioLanguage='" + mAudioLanguage + '\'' + - ", mContentRating='" + mContentRating + '\'' + - ", mPosterArtUri=" + mPosterArtUri + - ", mThumbnailUri=" + mThumbnailUri + - ", mSearchable=" + mSearchable + - ", mDataUri=" + mDataUri + - ", mDataBytes=" + mDataBytes + - ", mDurationMillis=" + mDurationMillis + - ", mExpireTimeUtcMillis=" + mExpireTimeUtcMillis + - ", mInternalProviderFlag1=" + mInternalProviderFlag1 + - ", mInternalProviderFlag2=" + mInternalProviderFlag2 + - ", mInternalProviderFlag3=" + mInternalProviderFlag3 + - ", mInternalProviderFlag4=" + mInternalProviderFlag4 + - ", mSeasonNumber=" + mSeasonNumber + - ", mSeasonTitle=" + mSeasonTitle + - ", mVersionNumber=" + mVersionNumber + - '}'; - } - - @Nullable - private static String safeToString(@Nullable Object o) { - return o == null ? null : o.toString(); - } - - @Nullable - private static String safeEncode(@Nullable String[] genres) { - return genres == null ? null : TvContract.Programs.Genres.encode(genres); - } - - /** - * Returns an array containing all of the elements in the list. - */ - public static RecordedProgram[] toArray(Collection recordedPrograms) { - return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]); - } -} diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java deleted file mode 100644 index c3d236b0..00000000 --- a/src/com/android/tv/dvr/RecordingTask.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import android.annotation.TargetApi; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.media.tv.TvRecordingClient.RecordingCallback; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.util.Log; -import android.widget.Toast; - -import com.android.tv.InputSessionManager; -import com.android.tv.InputSessionManager.RecordingSession; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; -import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; -import com.android.tv.util.Clock; -import com.android.tv.util.Utils; - -import java.util.Comparator; -import java.util.concurrent.TimeUnit; - -/** - * A Handler that actually starts and stop a recording at the right time. - * - *

This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}. - * There is only one looper so messages must be handled quickly or start a separate thread. - */ -@WorkerThread -@VisibleForTesting -@TargetApi(Build.VERSION_CODES.N) -public class RecordingTask extends RecordingCallback implements Handler.Callback, - DvrManager.Listener { - private static final String TAG = "RecordingTask"; - private static final boolean DEBUG = false; - - /** - * Compares the end time in ascending order. - */ - public static final Comparator END_TIME_COMPARATOR - = new Comparator() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); - } - }; - - /** - * Compares ID in ascending order. - */ - public static final Comparator ID_COMPARATOR - = new Comparator() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); - } - }; - - /** - * Compares the priority in ascending order. - */ - public static final Comparator PRIORITY_COMPARATOR - = new Comparator() { - @Override - public int compare(RecordingTask lhs, RecordingTask rhs) { - return Long.compare(lhs.getPriority(), rhs.getPriority()); - } - }; - - @VisibleForTesting - static final int MSG_INITIALIZE = 1; - @VisibleForTesting - static final int MSG_START_RECORDING = 2; - @VisibleForTesting - static final int MSG_STOP_RECORDING = 3; - /** - * Message to update schedule. - */ - public static final int MSG_UDPATE_SCHEDULE = 4; - - /** - * The time when the start command will be sent before the recording starts. - */ - public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3); - /** - * If the recording starts later than the scheduled start time or ends before the scheduled end - * time, it's considered as clipped. - */ - private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); - - @VisibleForTesting - enum State { - NOT_STARTED, - SESSION_ACQUIRED, - CONNECTION_PENDING, - CONNECTED, - RECORDING_STARTED, - RECORDING_STOP_REQUESTED, - FINISHED, - ERROR, - RELEASED, - } - private final InputSessionManager mSessionManager; - private final DvrManager mDvrManager; - private final Context mContext; - - private final WritableDvrDataManager mDataManager; - private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); - private RecordingSession mRecordingSession; - private Handler mHandler; - private ScheduledRecording mScheduledRecording; - private final Channel mChannel; - private State mState = State.NOT_STARTED; - private final Clock mClock; - private boolean mStartedWithClipping; - private Uri mRecordedProgramUri; - private boolean mCanceled; - - RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, - DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, Clock clock) { - mContext = context; - mScheduledRecording = scheduledRecording; - mChannel = channel; - mSessionManager = sessionManager; - mDataManager = dataManager; - mClock = clock; - mDvrManager = dvrManager; - - if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); - } - - public void setHandler(Handler handler) { - mHandler = handler; - } - - @Override - public boolean handleMessage(Message msg) { - if (DEBUG) Log.d(TAG, "handleMessage " + msg); - SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, - TAG, "Null handler trying to handle " + msg); - try { - switch (msg.what) { - case MSG_INITIALIZE: - handleInit(); - break; - case MSG_START_RECORDING: - handleStartRecording(); - break; - case MSG_STOP_RECORDING: - handleStopRecording(); - break; - case MSG_UDPATE_SCHEDULE: - handleUpdateSchedule((ScheduledRecording) msg.obj); - break; - case HandlerWrapper.MESSAGE_REMOVE: - mHandler.removeCallbacksAndMessages(null); - mHandler = null; - release(); - return false; - default: - SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); - break; - } - return true; - } catch (Exception e) { - Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); - failAndQuit(); - } - return false; - } - - @Override - public void onDisconnected(String inputId) { - if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); - if (mRecordingSession != null && mState != State.FINISHED) { - failAndQuit(); - } - } - - @Override - public void onConnectionFailed(String inputId) { - if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); - if (mRecordingSession != null) { - failAndQuit(); - } - } - - @Override - public void onTuned(Uri channelUri) { - if (DEBUG) Log.d(TAG, "onTuned"); - if (mRecordingSession == null) { - return; - } - mState = State.CONNECTED; - if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, - mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { - failAndQuit(); - } - } - - @Override - public void onRecordingStopped(Uri recordedProgramUri) { - if (DEBUG) Log.d(TAG, "onRecordingStopped"); - if (mRecordingSession == null) { - return; - } - mRecordedProgramUri = recordedProgramUri; - mState = State.FINISHED; - int state = ScheduledRecording.STATE_RECORDING_FINISHED; - if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS - > mClock.currentTimeMillis()) { - state = ScheduledRecording.STATE_RECORDING_CLIPPED; - } - updateRecordingState(state); - sendRemove(); - if (mCanceled) { - removeRecordedProgram(); - } - } - - @Override - public void onError(int reason) { - if (DEBUG) Log.d(TAG, "onError reason " + reason); - if (mRecordingSession == null) { - return; - } - switch (reason) { - case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE: - mMainThreadHandler.post(new Runnable() { - @Override - public void run() { - if (TvApplication.getSingletons(mContext).getMainActivityWrapper() - .isResumed()) { - Toast.makeText(mContext.getApplicationContext(), - R.string.dvr_error_insufficient_space_description, - Toast.LENGTH_LONG) - .show(); - } else { - Utils.setRecordingFailedReason(mContext.getApplicationContext(), - TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - } - } - }); - // Pass through - default: - failAndQuit(); - break; - } - } - - private void handleInit() { - if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); - if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { - Log.w(TAG, "End time already past, not recording " + mScheduledRecording); - failAndQuit(); - return; - } - if (mChannel == null) { - Log.w(TAG, "Null channel for " + mScheduledRecording); - failAndQuit(); - return; - } - if (mChannel.getId() != mScheduledRecording.getChannelId()) { - Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " - + mScheduledRecording); - failAndQuit(); - return; - } - - String inputId = mChannel.getInputId(); - mRecordingSession = mSessionManager.createRecordingSession(inputId, - "recordingTask-" + mScheduledRecording.getId(), this, - mHandler, mScheduledRecording.getEndTimeMs()); - mState = State.SESSION_ACQUIRED; - mDvrManager.addListener(this, mHandler); - mRecordingSession.tune(inputId, mChannel.getUri()); - mState = State.CONNECTION_PENDING; - } - - private void failAndQuit() { - if (DEBUG) Log.d(TAG, "failAndQuit"); - updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); - mState = State.ERROR; - sendRemove(); - } - - private void sendRemove() { - if (DEBUG) Log.d(TAG, "sendRemove"); - if (mHandler != null) { - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( - HandlerWrapper.MESSAGE_REMOVE)); - } - } - - private void handleStartRecording() { - if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); - long programId = mScheduledRecording.getProgramId(); - mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null - : TvContract.buildProgramUri(programId)); - updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS); - // If it starts late, it's clipped. - if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS - < mClock.currentTimeMillis()) { - mStartedWithClipping = true; - } - mState = State.RECORDING_STARTED; - - if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, - mScheduledRecording.getEndTimeMs())) { - failAndQuit(); - } - } - - private void handleStopRecording() { - if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); - mRecordingSession.stopRecording(); - mState = State.RECORDING_STOP_REQUESTED; - } - - private void handleUpdateSchedule(ScheduledRecording schedule) { - mScheduledRecording = schedule; - // Check end time only. The start time is checked in InputTaskScheduler. - if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { - if (mRecordingSession != null) { - mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); - } - if (mState == State.RECORDING_STARTED) { - mHandler.removeMessages(MSG_STOP_RECORDING); - if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { - failAndQuit(); - } - } - } - } - - @VisibleForTesting - State getState() { - return mState; - } - - private long getScheduleId() { - return mScheduledRecording.getId(); - } - - /** - * Returns the priority. - */ - public long getPriority() { - return mScheduledRecording.getPriority(); - } - - /** - * Returns the start time of the recording. - */ - public long getStartTimeMs() { - return mScheduledRecording.getStartTimeMs(); - } - - /** - * Returns the end time of the recording. - */ - public long getEndTimeMs() { - return mScheduledRecording.getEndTimeMs(); - } - - private void release() { - if (mRecordingSession != null) { - mSessionManager.releaseRecordingSession(mRecordingSession); - mRecordingSession = null; - } - mDvrManager.removeListener(this); - } - - private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { - long now = mClock.currentTimeMillis(); - long delay = Math.max(0L, when - now); - if (DEBUG) { - Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 - + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); - } - return mHandler.sendEmptyMessageDelayed(what, delay); - } - - private void updateRecordingState(@ScheduledRecording.RecordingState int state) { - if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); - mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) - .build(); - runOnMainThread(new Runnable() { - @Override - public void run() { - ScheduledRecording schedule = mDataManager.getScheduledRecording( - mScheduledRecording.getId()); - if (schedule == null) { - // Schedule has been deleted. Delete the recorded program. - removeRecordedProgram(); - } else { - // Update the state based on the object in DataManager in case when it has been - // updated. mScheduledRecording will be updated from - // onScheduledRecordingStateChanged. - mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) - .setState(state).build()); - } - } - }); - } - - @Override - public void onStopRecordingRequested(ScheduledRecording recording) { - if (recording.getId() != mScheduledRecording.getId()) { - return; - } - stop(); - } - - /** - * Starts the task. - */ - public void start() { - mHandler.sendEmptyMessage(MSG_INITIALIZE); - } - - /** - * Stops the task. - */ - public void stop() { - if (DEBUG) Log.d(TAG, "stop"); - switch (mState) { - case RECORDING_STARTED: - mHandler.removeMessages(MSG_STOP_RECORDING); - handleStopRecording(); - break; - case RECORDING_STOP_REQUESTED: - // Do nothing - break; - case NOT_STARTED: - case SESSION_ACQUIRED: - case CONNECTION_PENDING: - case CONNECTED: - case FINISHED: - case ERROR: - case RELEASED: - default: - sendRemove(); - break; - } - } - - /** - * Cancels the task - */ - public void cancel() { - if (DEBUG) Log.d(TAG, "cancel"); - mCanceled = true; - stop(); - removeRecordedProgram(); - } - - /** - * Clean up the task. - */ - public void cleanUp() { - if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { - updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); - } - release(); - if (mHandler != null) { - mHandler.removeCallbacksAndMessages(null); - } - } - - @Override - public String toString() { - return getClass().getName() + "(" + mScheduledRecording + ")"; - } - - private void removeRecordedProgram() { - runOnMainThread(new Runnable() { - @Override - public void run() { - if (mRecordedProgramUri != null) { - mDvrManager.removeRecordedProgram(mRecordedProgramUri); - } - } - }); - } - - private void runOnMainThread(Runnable runnable) { - if (Looper.myLooper() == Looper.getMainLooper()) { - runnable.run(); - } else { - mMainThreadHandler.post(runnable); - } - } -} diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/ScheduledProgramReaper.java deleted file mode 100644 index cd79a631..00000000 --- a/src/com/android/tv/dvr/ScheduledProgramReaper.java +++ /dev/null @@ -1,67 +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; - -import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; - -import com.android.tv.util.Clock; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Deletes {@link ScheduledRecording} older than {@value @DAYS} days. - */ -class ScheduledProgramReaper implements Runnable { - - @VisibleForTesting - static final int DAYS = 2; - private final WritableDvrDataManager mDvrDataManager; - private final Clock mClock; - - ScheduledProgramReaper(WritableDvrDataManager dvrDataManager, Clock clock) { - mDvrDataManager = dvrDataManager; - mClock = clock; - } - - @Override - @MainThread - public void run() { - long cutoff = mClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(DAYS); - List toRemove = new ArrayList<>(); - for (ScheduledRecording r : mDvrDataManager.getAllScheduledRecordings()) { - // Do not remove the schedules if it belongs to the series recording and was finished - // successfully. The schedule is necessary for checking the scheduled episode of the - // series recording. - if (r.getEndTimeMs() < cutoff - && (r.getSeriesRecordingId() == SeriesRecording.ID_NOT_SET - || r.getState() != ScheduledRecording.STATE_RECORDING_FINISHED)) { - toRemove.add(r); - } - } - for (ScheduledRecording r : mDvrDataManager.getDeletedSchedules()) { - if (r.getEndTimeMs() < cutoff) { - toRemove.add(r); - } - } - if (!toRemove.isEmpty()) { - mDvrDataManager.removeScheduledRecording(ScheduledRecording.toArray(toRemove)); - } - } -} diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java deleted file mode 100644 index 2bda10ea..00000000 --- a/src/com/android/tv/dvr/ScheduledRecording.java +++ /dev/null @@ -1,887 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.Range; - -import com.android.tv.R; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Channel; -import com.android.tv.data.Program; -import com.android.tv.dvr.provider.DvrContract.Schedules; -import com.android.tv.util.CompositeComparator; -import com.android.tv.util.Utils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Collection; -import java.util.Comparator; -import java.util.Objects; - -/** - * A data class for one recording contents. - */ -@VisibleForTesting -public final class ScheduledRecording implements Parcelable { - private static final String TAG = "ScheduledRecording"; - - /** - * Indicates that the ID is not assigned yet. - */ - public static final long ID_NOT_SET = 0; - - /** - * The default priority of the recording. - */ - public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; - - /** - * Compares the start time in ascending order. - */ - public static final Comparator START_TIME_COMPARATOR - = new Comparator() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); - } - }; - - /** - * Compares the end time in ascending order. - */ - public static final Comparator END_TIME_COMPARATOR - = new Comparator() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); - } - }; - - /** - * Compares ID in ascending order. The schedule with the larger ID was created later. - */ - public static final Comparator ID_COMPARATOR - = new Comparator() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mId, rhs.mId); - } - }; - - /** - * Compares the priority in ascending order. - */ - public static final Comparator PRIORITY_COMPARATOR - = new Comparator() { - @Override - public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - return Long.compare(lhs.mPriority, rhs.mPriority); - } - }; - - /** - * Compares start time in ascending order and then priority in descending order and then ID in - * descending order. - */ - public static final Comparator START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(), - ID_COMPARATOR.reversed()); - - /** - * Builds scheduled recordings from programs. - */ - public static Builder builder(String inputId, Program p) { - return new Builder() - .setInputId(inputId) - .setChannelId(p.getChannelId()) - .setStartTimeMs(p.getStartTimeUtcMillis()).setEndTimeMs(p.getEndTimeUtcMillis()) - .setProgramId(p.getId()) - .setProgramTitle(p.getTitle()) - .setSeasonNumber(p.getSeasonNumber()) - .setEpisodeNumber(p.getEpisodeNumber()) - .setEpisodeTitle(p.getEpisodeTitle()) - .setProgramDescription(p.getDescription()) - .setProgramLongDescription(p.getLongDescription()) - .setProgramPosterArtUri(p.getPosterArtUri()) - .setProgramThumbnailUri(p.getThumbnailUri()) - .setType(TYPE_PROGRAM); - } - - public static Builder builder(String inputId, long channelId, long startTime, long endTime) { - return new Builder() - .setInputId(inputId) - .setChannelId(channelId) - .setStartTimeMs(startTime) - .setEndTimeMs(endTime) - .setType(TYPE_TIMED); - } - - /** - * Creates a new Builder with the values set from the {@link RecordedProgram}. - */ - @VisibleForTesting - public static Builder builder(RecordedProgram p) { - boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle()); - return new Builder() - .setInputId(p.getInputId()) - .setChannelId(p.getChannelId()) - .setType(isProgramRecording ? TYPE_PROGRAM : TYPE_TIMED) - .setStartTimeMs(p.getStartTimeUtcMillis()) - .setEndTimeMs(p.getEndTimeUtcMillis()) - .setProgramTitle(p.getTitle()) - .setSeasonNumber(p.getSeasonNumber()) - .setEpisodeNumber(p.getEpisodeNumber()) - .setEpisodeTitle(p.getEpisodeTitle()) - .setProgramDescription(p.getDescription()) - .setProgramLongDescription(p.getLongDescription()) - .setProgramPosterArtUri(p.getPosterArtUri()) - .setProgramThumbnailUri(p.getThumbnailUri()) - .setState(STATE_RECORDING_FINISHED); - } - - public static final class Builder { - private long mId = ID_NOT_SET; - private long mPriority = DvrScheduleManager.DEFAULT_PRIORITY; - private String mInputId; - private long mChannelId; - private long mProgramId = ID_NOT_SET; - private String mProgramTitle; - private @RecordingType int mType; - private long mStartTimeMs; - private long mEndTimeMs; - private String mSeasonNumber; - private String mEpisodeNumber; - private String mEpisodeTitle; - private String mProgramDescription; - private String mProgramLongDescription; - private String mProgramPosterArtUri; - private String mProgramThumbnailUri; - private @RecordingState int mState; - private long mSeriesRecordingId = ID_NOT_SET; - - private Builder() { } - - public Builder setId(long id) { - mId = id; - return this; - } - - public Builder setPriority(long priority) { - mPriority = priority; - return this; - } - - public Builder setInputId(String inputId) { - mInputId = inputId; - return this; - } - - public Builder setChannelId(long channelId) { - mChannelId = channelId; - return this; - } - - public Builder setProgramId(long programId) { - mProgramId = programId; - return this; - } - - public Builder setProgramTitle(String programTitle) { - mProgramTitle = programTitle; - return this; - } - - private Builder setType(@RecordingType int type) { - mType = type; - return this; - } - - public Builder setStartTimeMs(long startTimeMs) { - mStartTimeMs = startTimeMs; - return this; - } - - public Builder setEndTimeMs(long endTimeMs) { - mEndTimeMs = endTimeMs; - return this; - } - - public Builder setSeasonNumber(String seasonNumber) { - mSeasonNumber = seasonNumber; - return this; - } - - public Builder setEpisodeNumber(String episodeNumber) { - mEpisodeNumber = episodeNumber; - return this; - } - - public Builder setEpisodeTitle(String episodeTitle) { - mEpisodeTitle = episodeTitle; - return this; - } - - public Builder setProgramDescription(String description) { - mProgramDescription = description; - return this; - } - - public Builder setProgramLongDescription(String longDescription) { - mProgramLongDescription = longDescription; - return this; - } - - public Builder setProgramPosterArtUri(String programPosterArtUri) { - mProgramPosterArtUri = programPosterArtUri; - return this; - } - - public Builder setProgramThumbnailUri(String programThumbnailUri) { - mProgramThumbnailUri = programThumbnailUri; - return this; - } - - public Builder setState(@RecordingState int state) { - mState = state; - return this; - } - - public Builder setSeriesRecordingId(long seriesRecordingId) { - mSeriesRecordingId = seriesRecordingId; - return this; - } - - public ScheduledRecording build() { - return new ScheduledRecording(mId, mPriority, mInputId, mChannelId, mProgramId, - mProgramTitle, mType, mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, - mEpisodeTitle, mProgramDescription, mProgramLongDescription, - mProgramPosterArtUri, mProgramThumbnailUri, mState, mSeriesRecordingId); - } - } - - /** - * Creates {@link Builder} object from the given original {@code Recording}. - */ - public static Builder buildFrom(ScheduledRecording orig) { - return new Builder() - .setId(orig.mId) - .setInputId(orig.mInputId) - .setChannelId(orig.mChannelId) - .setEndTimeMs(orig.mEndTimeMs) - .setSeriesRecordingId(orig.mSeriesRecordingId) - .setPriority(orig.mPriority) - .setProgramId(orig.mProgramId) - .setProgramTitle(orig.mProgramTitle) - .setStartTimeMs(orig.mStartTimeMs) - .setSeasonNumber(orig.getSeasonNumber()) - .setEpisodeNumber(orig.getEpisodeNumber()) - .setEpisodeTitle(orig.getEpisodeTitle()) - .setProgramDescription(orig.getProgramDescription()) - .setProgramLongDescription(orig.getProgramLongDescription()) - .setProgramPosterArtUri(orig.getProgramPosterArtUri()) - .setProgramThumbnailUri(orig.getProgramThumbnailUri()) - .setState(orig.mState).setType(orig.mType); - } - - @Retention(RetentionPolicy.SOURCE) - @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS, STATE_RECORDING_FINISHED, - STATE_RECORDING_FAILED, STATE_RECORDING_CLIPPED, STATE_RECORDING_DELETED, - STATE_RECORDING_CANCELED}) - public @interface RecordingState {} - public static final int STATE_RECORDING_NOT_STARTED = 0; - public static final int STATE_RECORDING_IN_PROGRESS = 1; - public static final int STATE_RECORDING_FINISHED = 2; - public static final int STATE_RECORDING_FAILED = 3; - public static final int STATE_RECORDING_CLIPPED = 4; - public static final int STATE_RECORDING_DELETED = 5; - public static final int STATE_RECORDING_CANCELED = 6; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_TIMED, TYPE_PROGRAM}) - public @interface RecordingType {} - /** - * Record with given time range. - */ - public static final int TYPE_TIMED = 1; - /** - * Record with a given program. - */ - public static final int TYPE_PROGRAM = 2; - - @RecordingType private final int mType; - - /** - * Use this projection if you want to create {@link ScheduledRecording} object using - * {@link #fromCursor}. - */ - public static final String[] PROJECTION = { - // Columns must match what is read in #fromCursor - Schedules._ID, - Schedules.COLUMN_PRIORITY, - Schedules.COLUMN_TYPE, - Schedules.COLUMN_INPUT_ID, - Schedules.COLUMN_CHANNEL_ID, - Schedules.COLUMN_PROGRAM_ID, - Schedules.COLUMN_PROGRAM_TITLE, - Schedules.COLUMN_START_TIME_UTC_MILLIS, - Schedules.COLUMN_END_TIME_UTC_MILLIS, - Schedules.COLUMN_SEASON_NUMBER, - Schedules.COLUMN_EPISODE_NUMBER, - Schedules.COLUMN_EPISODE_TITLE, - Schedules.COLUMN_PROGRAM_DESCRIPTION, - Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, - Schedules.COLUMN_PROGRAM_POST_ART_URI, - Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, - Schedules.COLUMN_STATE, - Schedules.COLUMN_SERIES_RECORDING_ID}; - - /** - * Creates {@link ScheduledRecording} object from the given {@link Cursor}. - */ - public static ScheduledRecording fromCursor(Cursor c) { - int index = -1; - return new Builder() - .setId(c.getLong(++index)) - .setPriority(c.getLong(++index)) - .setType(recordingType(c.getString(++index))) - .setInputId(c.getString(++index)) - .setChannelId(c.getLong(++index)) - .setProgramId(c.getLong(++index)) - .setProgramTitle(c.getString(++index)) - .setStartTimeMs(c.getLong(++index)) - .setEndTimeMs(c.getLong(++index)) - .setSeasonNumber(c.getString(++index)) - .setEpisodeNumber(c.getString(++index)) - .setEpisodeTitle(c.getString(++index)) - .setProgramDescription(c.getString(++index)) - .setProgramLongDescription(c.getString(++index)) - .setProgramPosterArtUri(c.getString(++index)) - .setProgramThumbnailUri(c.getString(++index)) - .setState(recordingState(c.getString(++index))) - .setSeriesRecordingId(c.getLong(++index)) - .build(); - } - - public static ContentValues toContentValues(ScheduledRecording r) { - ContentValues values = new ContentValues(); - if (r.getId() != ID_NOT_SET) { - values.put(Schedules._ID, r.getId()); - } - values.put(Schedules.COLUMN_INPUT_ID, r.getInputId()); - values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId()); - values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId()); - values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle()); - values.put(Schedules.COLUMN_PRIORITY, r.getPriority()); - values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs()); - values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs()); - values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber()); - values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber()); - values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle()); - values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription()); - values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription()); - values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri()); - values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri()); - values.put(Schedules.COLUMN_STATE, recordingState(r.getState())); - values.put(Schedules.COLUMN_TYPE, recordingType(r.getType())); - if (r.getSeriesRecordingId() != ID_NOT_SET) { - values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId()); - } else { - values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID); - } - return values; - } - - public static ScheduledRecording fromParcel(Parcel in) { - return new Builder() - .setId(in.readLong()) - .setPriority(in.readLong()) - .setInputId(in.readString()) - .setChannelId(in.readLong()) - .setProgramId(in.readLong()) - .setProgramTitle(in.readString()) - .setType(in.readInt()) - .setStartTimeMs(in.readLong()) - .setEndTimeMs(in.readLong()) - .setSeasonNumber(in.readString()) - .setEpisodeNumber(in.readString()) - .setEpisodeTitle(in.readString()) - .setProgramDescription(in.readString()) - .setProgramLongDescription(in.readString()) - .setProgramPosterArtUri(in.readString()) - .setProgramThumbnailUri(in.readString()) - .setState(in.readInt()) - .setSeriesRecordingId(in.readLong()) - .build(); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public ScheduledRecording createFromParcel(Parcel in) { - return ScheduledRecording.fromParcel(in); - } - - @Override - public ScheduledRecording[] newArray(int size) { - return new ScheduledRecording[size]; - } - }; - - /** - * The ID internal to Live TV - */ - private long mId; - - /** - * The priority of this recording. - * - *

The highest number is recorded first. If there is a tie in priority then the higher id - * wins. - */ - private final long mPriority; - - private final String mInputId; - private final long mChannelId; - /** - * Optional id of the associated program. - */ - private final long mProgramId; - private final String mProgramTitle; - - private final long mStartTimeMs; - private final long mEndTimeMs; - private final String mSeasonNumber; - private final String mEpisodeNumber; - private final String mEpisodeTitle; - private final String mProgramDescription; - private final String mProgramLongDescription; - private final String mProgramPosterArtUri; - private final String mProgramThumbnailUri; - @RecordingState private final int mState; - private final long mSeriesRecordingId; - - private ScheduledRecording(long id, long priority, String inputId, long channelId, long programId, - String programTitle, @RecordingType int type, long startTime, long endTime, - String seasonNumber, String episodeNumber, String episodeTitle, - String programDescription, String programLongDescription, String programPosterArtUri, - String programThumbnailUri, @RecordingState int state, long seriesRecordingId) { - mId = id; - mPriority = priority; - mInputId = inputId; - mChannelId = channelId; - mProgramId = programId; - mProgramTitle = programTitle; - mType = type; - mStartTimeMs = startTime; - mEndTimeMs = endTime; - mSeasonNumber = seasonNumber; - mEpisodeNumber = episodeNumber; - mEpisodeTitle = episodeTitle; - mProgramDescription = programDescription; - mProgramLongDescription = programLongDescription; - mProgramPosterArtUri = programPosterArtUri; - mProgramThumbnailUri = programThumbnailUri; - mState = state; - mSeriesRecordingId = seriesRecordingId; - } - - /** - * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and - * {@link #TYPE_TIMED}. - */ - @RecordingType - public int getType() { - return mType; - } - - /** - * Returns schedules' input id. - */ - public String getInputId() { - return mInputId; - } - - /** - * Returns recorded {@link Channel}. - */ - public long getChannelId() { - return mChannelId; - } - - /** - * Return the optional program id - */ - public long getProgramId() { - return mProgramId; - } - - /** - * Return the optional program Title - */ - public String getProgramTitle() { - return mProgramTitle; - } - - /** - * Returns started time. - */ - public long getStartTimeMs() { - return mStartTimeMs; - } - - /** - * Returns ended time. - */ - public long getEndTimeMs() { - return mEndTimeMs; - } - - /** - * Returns the season number. - */ - public String getSeasonNumber() { - return mSeasonNumber; - } - - /** - * Returns the episode number. - */ - public String getEpisodeNumber() { - return mEpisodeNumber; - } - - /** - * Returns the episode title. - */ - public String getEpisodeTitle() { - return mEpisodeTitle; - } - - /** - * Returns the description of program. - */ - public String getProgramDescription() { - return mProgramDescription; - } - - /** - * Returns the long description of program. - */ - public String getProgramLongDescription() { - return mProgramLongDescription; - } - - /** - * Returns the poster uri of program. - */ - public String getProgramPosterArtUri() { - return mProgramPosterArtUri; - } - - /** - * Returns the thumb nail uri of program. - */ - public String getProgramThumbnailUri() { - return mProgramThumbnailUri; - } - - /** - * Returns duration. - */ - public long getDuration() { - return mEndTimeMs - mStartTimeMs; - } - - /** - * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED}, - * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, - * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and - * {@link #STATE_RECORDING_DELETED}. - */ - @RecordingState public int getState() { - return mState; - } - - /** - * Returns the ID of the {@link SeriesRecording} including this schedule. - */ - public long getSeriesRecordingId() { - return mSeriesRecordingId; - } - - public long getId() { - return mId; - } - - /** - * Sets the ID; - */ - public void setId(long id) { - mId = id; - } - - public long getPriority() { - return mPriority; - } - - /** - * Returns season number, episode number and episode title for display. - */ - public String getEpisodeDisplayTitle(Context context) { - if (!TextUtils.isEmpty(mEpisodeNumber)) { - String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; - if (TextUtils.equals(mSeasonNumber, "0")) { - // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_title_format_no_season_number), - mEpisodeNumber, episodeTitle); - } else { - return String.format(context.getResources().getString( - R.string.display_episode_title_format), - mSeasonNumber, mEpisodeNumber, episodeTitle); - } - } - return mEpisodeTitle; - } - - /** - * Returns the program's title withe its season and episode number. - */ - public String getProgramTitleWithEpisodeNumber(Context context) { - if (TextUtils.isEmpty(mProgramTitle)) { - return mProgramTitle; - } - if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { - return TextUtils.isEmpty(mEpisodeNumber) ? mProgramTitle : context.getString( - R.string.program_title_with_episode_number_no_season, mProgramTitle, - mEpisodeNumber); - } else { - return context.getString(R.string.program_title_with_episode_number, mProgramTitle, - mSeasonNumber, mEpisodeNumber); - } - } - - - /** - * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. - */ - private static @RecordingType int recordingType(String type) { - switch (type) { - case Schedules.TYPE_TIMED: - return TYPE_TIMED; - case Schedules.TYPE_PROGRAM: - return TYPE_PROGRAM; - default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); - return TYPE_TIMED; - } - } - - /** - * Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}. - */ - private static String recordingType(@RecordingType int type) { - switch (type) { - case TYPE_TIMED: - return Schedules.TYPE_TIMED; - case TYPE_PROGRAM: - return Schedules.TYPE_PROGRAM; - default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); - return Schedules.TYPE_TIMED; - } - } - - /** - * Converts a string to a @RecordingState int, defaulting to - * {@link #STATE_RECORDING_NOT_STARTED}. - */ - private static @RecordingState int recordingState(String state) { - switch (state) { - case Schedules.STATE_RECORDING_NOT_STARTED: - return STATE_RECORDING_NOT_STARTED; - case Schedules.STATE_RECORDING_IN_PROGRESS: - return STATE_RECORDING_IN_PROGRESS; - case Schedules.STATE_RECORDING_FINISHED: - return STATE_RECORDING_FINISHED; - case Schedules.STATE_RECORDING_FAILED: - return STATE_RECORDING_FAILED; - case Schedules.STATE_RECORDING_CLIPPED: - return STATE_RECORDING_CLIPPED; - case Schedules.STATE_RECORDING_DELETED: - return STATE_RECORDING_DELETED; - case Schedules.STATE_RECORDING_CANCELED: - return STATE_RECORDING_CANCELED; - default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); - return STATE_RECORDING_NOT_STARTED; - } - } - - /** - * Converts a @RecordingState int to string, defaulting to - * {@link Schedules#STATE_RECORDING_NOT_STARTED}. - */ - private static String recordingState(@RecordingState int state) { - switch (state) { - case STATE_RECORDING_NOT_STARTED: - return Schedules.STATE_RECORDING_NOT_STARTED; - case STATE_RECORDING_IN_PROGRESS: - return Schedules.STATE_RECORDING_IN_PROGRESS; - case STATE_RECORDING_FINISHED: - return Schedules.STATE_RECORDING_FINISHED; - case STATE_RECORDING_FAILED: - return Schedules.STATE_RECORDING_FAILED; - case STATE_RECORDING_CLIPPED: - return Schedules.STATE_RECORDING_CLIPPED; - case STATE_RECORDING_DELETED: - return Schedules.STATE_RECORDING_DELETED; - case STATE_RECORDING_CANCELED: - return Schedules.STATE_RECORDING_CANCELED; - default: - SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); - return Schedules.STATE_RECORDING_NOT_STARTED; - } - } - - /** - * Checks if the {@code period} overlaps with the recording time. - */ - public boolean isOverLapping(Range period) { - return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); - } - - /** - * Checks if the {@code schedule} overlaps with this schedule. - */ - public boolean isOverLapping(ScheduledRecording schedule) { - return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs(); - } - - @Override - public String toString() { - return "ScheduledRecording[" + mId - + "]" - + "(inputId=" + mInputId - + ",channelId=" + mChannelId - + ",programId=" + mProgramId - + ",programTitle=" + mProgramTitle - + ",type=" + mType - + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")" - + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")" - + ",seasonNumber=" + mSeasonNumber - + ",episodeNumber=" + mEpisodeNumber - + ",episodeTitle=" + mEpisodeTitle - + ",programDescription=" + mProgramDescription - + ",programLongDescription=" + mProgramLongDescription - + ",programPosterArtUri=" + mProgramPosterArtUri - + ",programThumbnailUri=" + mProgramThumbnailUri - + ",state=" + mState - + ",priority=" + mPriority - + ",seriesRecordingId=" + mSeriesRecordingId - + ")"; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int paramInt) { - out.writeLong(mId); - out.writeLong(mPriority); - out.writeString(mInputId); - out.writeLong(mChannelId); - out.writeLong(mProgramId); - out.writeString(mProgramTitle); - out.writeInt(mType); - out.writeLong(mStartTimeMs); - out.writeLong(mEndTimeMs); - out.writeString(mSeasonNumber); - out.writeString(mEpisodeNumber); - out.writeString(mEpisodeTitle); - out.writeString(mProgramDescription); - out.writeString(mProgramLongDescription); - out.writeString(mProgramPosterArtUri); - out.writeString(mProgramThumbnailUri); - out.writeInt(mState); - out.writeLong(mSeriesRecordingId); - } - - /** - * Returns {@code true} if the recording is not started yet, otherwise @{code false}. - */ - public boolean isNotStarted() { - return mState == STATE_RECORDING_NOT_STARTED; - } - - /** - * Returns {@code true} if the recording is in progress, otherwise @{code false}. - */ - public boolean isInProgress() { - return mState == STATE_RECORDING_IN_PROGRESS; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ScheduledRecording)) { - return false; - } - ScheduledRecording r = (ScheduledRecording) obj; - return mId == r.mId - && mPriority == r.mPriority - && mChannelId == r.mChannelId - && mProgramId == r.mProgramId - && Objects.equals(mProgramTitle, r.mProgramTitle) - && mType == r.mType - && mStartTimeMs == r.mStartTimeMs - && mEndTimeMs == r.mEndTimeMs - && Objects.equals(mSeasonNumber, r.mSeasonNumber) - && Objects.equals(mEpisodeNumber, r.mEpisodeNumber) - && Objects.equals(mEpisodeTitle, r.mEpisodeTitle) - && Objects.equals(mProgramDescription, r.getProgramDescription()) - && Objects.equals(mProgramLongDescription, r.getProgramLongDescription()) - && Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri()) - && Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri()) - && mState == r.mState - && mSeriesRecordingId == r.mSeriesRecordingId; - } - - @Override - public int hashCode() { - return Objects.hash(mId, mPriority, mChannelId, mProgramId, mProgramTitle, mType, - mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, mEpisodeTitle, - mProgramDescription, mProgramLongDescription, mProgramPosterArtUri, - mProgramThumbnailUri, mState, mSeriesRecordingId); - } - - /** - * Returns an array containing all of the elements in the list. - */ - public static ScheduledRecording[] toArray(Collection schedules) { - return schedules.toArray(new ScheduledRecording[schedules.size()]); - } -} diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java deleted file mode 100644 index ce78e1be..00000000 --- a/src/com/android/tv/dvr/Scheduler.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager.TvInputCallback; -import android.os.Looper; -import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; -import android.util.ArrayMap; -import android.util.Log; -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.OnDvrScheduleLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.util.Clock; -import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.Utils; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * The core class to manage schedule and run actual recording. - */ -@MainThread -public class Scheduler extends TvInputCallback implements ScheduledRecordingListener { - private static final String TAG = "Scheduler"; - private static final boolean DEBUG = false; - - private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5); - @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1); - - private final Looper mLooper; - private final InputSessionManager mSessionManager; - private final WritableDvrDataManager mDataManager; - private final DvrManager mDvrManager; - private final ChannelDataManager mChannelDataManager; - private final TvInputManagerHelper mInputManager; - private final Context mContext; - private final Clock mClock; - private final AlarmManager mAlarmManager; - - private final Map mInputSchedulerMap = new ArrayMap<>(); - private long mLastStartTimePendingMs; - - public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, - WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, - TvInputManagerHelper inputManager, Context context, Clock clock, - AlarmManager alarmManager) { - mLooper = looper; - mDvrManager = dvrManager; - mSessionManager = sessionManager; - mDataManager = dataManager; - mChannelDataManager = channelDataManager; - mInputManager = inputManager; - mContext = context; - mClock = clock; - mAlarmManager = alarmManager; - } - - /** - * Starts the scheduler. - */ - public void start() { - mDataManager.addScheduledRecordingListener(this); - mInputManager.addCallback(this); - if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { - updateInternal(); - } else { - if (!mDataManager.isDvrScheduleLoadFinished()) { - mDataManager.addDvrScheduleLoadFinishedListener( - new OnDvrScheduleLoadFinishedListener() { - @Override - public void onDvrScheduleLoadFinished() { - mDataManager.removeDvrScheduleLoadFinishedListener(this); - updateInternal(); - } - }); - } - if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener(new Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - updateInternal(); - } - - @Override - public void onChannelListUpdated() { } - - @Override - public void onChannelBrowsableChanged() { } - }); - } - } - } - - /** - * Stops the scheduler. - */ - public void stop() { - for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { - inputTaskScheduler.stop(); - } - mInputManager.removeCallback(this); - mDataManager.removeScheduledRecordingListener(this); - } - - private void updatePendingRecordings() { - List scheduledRecordings = mDataManager - .getScheduledRecordings(new Range<>(mLastStartTimePendingMs, - mClock.currentTimeMillis() + SOON_DURATION_IN_MS), - ScheduledRecording.STATE_RECORDING_NOT_STARTED); - for (ScheduledRecording r : scheduledRecordings) { - scheduleRecordingSoon(r); - } - } - - /** - * Start recording that will happen soon, and set the next alarm time. - */ - public void update() { - if (DEBUG) Log.d(TAG, "update"); - updateInternal(); - } - - private void updateInternal() { - if (isInitialized()) { - updatePendingRecordings(); - updateNextAlarm(); - } - } - - private boolean isInitialized() { - return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); - } - - @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { - if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules)); - if (!isInitialized()) { - return; - } - handleScheduleChange(schedules); - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { - if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules)); - if (!isInitialized()) { - return; - } - boolean needToUpdateAlarm = false; - for (ScheduledRecording schedule : schedules) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); - if (scheduler != null) { - scheduler.removeSchedule(schedule); - needToUpdateAlarm = true; - } - } - if (needToUpdateAlarm) { - updateNextAlarm(); - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { - if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules)); - if (!isInitialized()) { - return; - } - // Update the recordings. - for (ScheduledRecording schedule : schedules) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); - if (scheduler != null) { - scheduler.updateSchedule(schedule); - } - } - handleScheduleChange(schedules); - } - - private void handleScheduleChange(ScheduledRecording... schedules) { - boolean needToUpdateAlarm = false; - for (ScheduledRecording schedule : schedules) { - if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - if (startsWithin(schedule, SOON_DURATION_IN_MS)) { - scheduleRecordingSoon(schedule); - } else { - needToUpdateAlarm = true; - } - } - } - if (needToUpdateAlarm) { - updateNextAlarm(); - } - } - - private void scheduleRecordingSoon(ScheduledRecording schedule) { - TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); - if (input == null) { - Log.e(TAG, "Can't find input for " + schedule); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); - return; - } - if (!input.canRecord() || input.getTunerCount() <= 0) { - Log.e(TAG, "TV input doesn't support recording: " + input); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); - return; - } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); - if (scheduler == null) { - scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager, - mDvrManager, mDataManager, mSessionManager, mClock); - mInputSchedulerMap.put(input.getId(), scheduler); - } - scheduler.addSchedule(schedule); - if (mLastStartTimePendingMs < schedule.getStartTimeMs()) { - mLastStartTimePendingMs = schedule.getStartTimeMs(); - } - } - - private void updateNextAlarm() { - long nextStartTime = mDataManager.getNextScheduledStartTimeAfter( - Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); - if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { - long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; - if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); - Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); - PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); - // This will cancel the previous alarm. - mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); - } else { - if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); - } - } - - @VisibleForTesting - boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { - return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; - } - - // No need to remove input task scheduler when the input is removed. If the input is removed - // temporarily, the scheduler should keep the non-started schedules. - @Override - public void onInputUpdated(String inputId) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId); - if (scheduler != null) { - scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); - } - } - - @Override - public void onTvInputInfoUpdated(TvInputInfo input) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); - if (scheduler != null) { - scheduler.updateTvInputInfo(input); - } - } -} diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/SeriesInfo.java deleted file mode 100644 index 30256dc5..00000000 --- a/src/com/android/tv/dvr/SeriesInfo.java +++ /dev/null @@ -1,76 +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; - -/** - * Series information. - */ -public class SeriesInfo { - private final String mId; - private final String mTitle; - private final String mDescription; - private final String mLongDescription; - private final int[] mCanonicalGenreIds; - private final String mPosterUri; - private final String mPhotoUri; - - public SeriesInfo(String id, String title, String description, String longDescription, - int[] canonicalGenreIds, String posterUri, String photoUri) { - this.mId = id; - this.mTitle = title; - this.mDescription = description; - this.mLongDescription = longDescription; - this.mCanonicalGenreIds = canonicalGenreIds; - this.mPosterUri = posterUri; - this.mPhotoUri = photoUri; - } - - /** Returns the ID. **/ - public String getId() { - return mId; - } - - /** Returns the title. **/ - public String getTitle() { - return mTitle; - } - - /** Returns the description. **/ - public String getDescription() { - return mDescription; - } - - /** Returns the description. **/ - public String getLongDescription() { - return mLongDescription; - } - - /** Returns the canonical genre IDs. **/ - public int[] getCanonicalGenreIds() { - return mCanonicalGenreIds; - } - - /** Returns the poster URI. **/ - public String getPosterUri() { - return mPosterUri; - } - - /** Returns the photo URI. **/ - public String getPhotoUri() { - return mPhotoUri; - } -} diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java deleted file mode 100644 index f0690f5f..00000000 --- a/src/com/android/tv/dvr/SeriesRecording.java +++ /dev/null @@ -1,755 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr; - -import android.content.ContentValues; -import android.database.Cursor; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.IntDef; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; - -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Program; -import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; -import com.android.tv.util.Utils; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Objects; - -/** - * Schedules the recording of a Series of Programs. - * - *

- * Contains the data needed to create new ScheduleRecordings as the programs become available in - * the EPG. - */ -public class SeriesRecording implements Parcelable { - /** - * Indicates that the ID is not assigned yet. - */ - public static final long ID_NOT_SET = 0; - - /** - * The default priority of this recording. - */ - public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}) - public @interface ChannelOption {} - /** - * An option which indicates that the episodes in one channel are recorded. - */ - public static final int OPTION_CHANNEL_ONE = 0; - /** - * An option which indicates that the episodes in all the channels are recorded. - */ - public static final int OPTION_CHANNEL_ALL = 1; - - @Retention(RetentionPolicy.SOURCE) - @IntDef(flag = true, - value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) - public @interface SeriesState {} - - /** - * The state indicates that the series recording is a normal one. - */ - public static final int STATE_SERIES_NORMAL = 0; - - /** - * The state indicates that the series recording is stopped. - */ - public static final int STATE_SERIES_STOPPED = 1; - - /** - * Compare priority in descending order. - */ - public static final Comparator PRIORITY_COMPARATOR = - new Comparator() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - int value = Long.compare(rhs.mPriority, lhs.mPriority); - if (value == 0) { - // New recording has the higher priority. - value = Long.compare(rhs.mId, lhs.mId); - } - return value; - } - }; - - /** - * Compare ID in ascending order. - */ - public static final Comparator ID_COMPARATOR = - new Comparator() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - return Long.compare(lhs.mId, rhs.mId); - } - }; - - /** - * Creates a new Builder with the values set from the series information of {@link BaseProgram}. - */ - public static Builder builder(String inputId, BaseProgram p) { - return new Builder() - .setInputId(inputId) - .setSeriesId(p.getSeriesId()) - .setChannelId(p.getChannelId()) - .setTitle(p.getTitle()) - .setDescription(p.getDescription()) - .setLongDescription(p.getLongDescription()) - .setCanonicalGenreIds(p.getCanonicalGenreIds()) - .setPosterUri(p.getPosterArtUri()) - .setPhotoUri(p.getThumbnailUri()); - } - - /** - * Creates a new Builder with the values set from an existing {@link SeriesRecording}. - */ - @VisibleForTesting - public static Builder buildFrom(SeriesRecording r) { - return new Builder() - .setId(r.mId) - .setInputId(r.getInputId()) - .setChannelId(r.getChannelId()) - .setPriority(r.getPriority()) - .setTitle(r.getTitle()) - .setDescription(r.getDescription()) - .setLongDescription(r.getLongDescription()) - .setSeriesId(r.getSeriesId()) - .setStartFromEpisode(r.getStartFromEpisode()) - .setStartFromSeason(r.getStartFromSeason()) - .setChannelOption(r.getChannelOption()) - .setCanonicalGenreIds(r.getCanonicalGenreIds()) - .setPosterUri(r.getPosterUri()) - .setPhotoUri(r.getPhotoUri()) - .setState(r.getState()); - } - - /** - * Use this projection if you want to create {@link SeriesRecording} object using - * {@link #fromCursor}. - */ - public static final String[] PROJECTION = { - // Columns must match what is read in fromCursor() - SeriesRecordings._ID, - SeriesRecordings.COLUMN_INPUT_ID, - SeriesRecordings.COLUMN_CHANNEL_ID, - SeriesRecordings.COLUMN_PRIORITY, - SeriesRecordings.COLUMN_TITLE, - SeriesRecordings.COLUMN_SHORT_DESCRIPTION, - SeriesRecordings.COLUMN_LONG_DESCRIPTION, - SeriesRecordings.COLUMN_SERIES_ID, - SeriesRecordings.COLUMN_START_FROM_EPISODE, - SeriesRecordings.COLUMN_START_FROM_SEASON, - SeriesRecordings.COLUMN_CHANNEL_OPTION, - SeriesRecordings.COLUMN_CANONICAL_GENRE, - SeriesRecordings.COLUMN_POSTER_URI, - SeriesRecordings.COLUMN_PHOTO_URI, - SeriesRecordings.COLUMN_STATE - }; - /** - * Creates {@link SeriesRecording} object from the given {@link Cursor}. - */ - public static SeriesRecording fromCursor(Cursor c) { - int index = -1; - return new Builder() - .setId(c.getLong(++index)) - .setInputId(c.getString(++index)) - .setChannelId(c.getLong(++index)) - .setPriority(c.getLong(++index)) - .setTitle(c.getString(++index)) - .setDescription(c.getString(++index)) - .setLongDescription(c.getString(++index)) - .setSeriesId(c.getString(++index)) - .setStartFromEpisode(c.getInt(++index)) - .setStartFromSeason(c.getInt(++index)) - .setChannelOption(channelOption(c.getString(++index))) - .setCanonicalGenreIds(c.getString(++index)) - .setPosterUri(c.getString(++index)) - .setPhotoUri(c.getString(++index)) - .setState(seriesRecordingState(c.getString(++index))) - .build(); - } - - /** - * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings} - * and the values from {@code r}. - */ - public static ContentValues toContentValues(SeriesRecording r) { - ContentValues values = new ContentValues(); - if (r.getId() != ID_NOT_SET) { - values.put(SeriesRecordings._ID, r.getId()); - } else { - values.putNull(SeriesRecordings._ID); - } - values.put(SeriesRecordings.COLUMN_INPUT_ID, r.getInputId()); - values.put(SeriesRecordings.COLUMN_CHANNEL_ID, r.getChannelId()); - values.put(SeriesRecordings.COLUMN_PRIORITY, r.getPriority()); - values.put(SeriesRecordings.COLUMN_TITLE, r.getTitle()); - values.put(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, r.getDescription()); - values.put(SeriesRecordings.COLUMN_LONG_DESCRIPTION, r.getLongDescription()); - values.put(SeriesRecordings.COLUMN_SERIES_ID, r.getSeriesId()); - values.put(SeriesRecordings.COLUMN_START_FROM_EPISODE, r.getStartFromEpisode()); - values.put(SeriesRecordings.COLUMN_START_FROM_SEASON, r.getStartFromSeason()); - values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION, - channelOption(r.getChannelOption())); - values.put(SeriesRecordings.COLUMN_CANONICAL_GENRE, - Utils.getCanonicalGenre(r.getCanonicalGenreIds())); - values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri()); - values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri()); - values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState())); - return values; - } - - private static String channelOption(@ChannelOption int option) { - switch (option) { - case OPTION_CHANNEL_ONE: - return SeriesRecordings.OPTION_CHANNEL_ONE; - case OPTION_CHANNEL_ALL: - return SeriesRecordings.OPTION_CHANNEL_ALL; - } - return SeriesRecordings.OPTION_CHANNEL_ONE; - } - - @ChannelOption private static int channelOption(String option) { - switch (option) { - case SeriesRecordings.OPTION_CHANNEL_ONE: - return OPTION_CHANNEL_ONE; - case SeriesRecordings.OPTION_CHANNEL_ALL: - return OPTION_CHANNEL_ALL; - } - return OPTION_CHANNEL_ONE; - } - - private static String seriesRecordingState(@SeriesState int state) { - switch (state) { - case STATE_SERIES_NORMAL: - return SeriesRecordings.STATE_SERIES_NORMAL; - case STATE_SERIES_STOPPED: - return SeriesRecordings.STATE_SERIES_STOPPED; - } - return SeriesRecordings.STATE_SERIES_NORMAL; - } - - @SeriesState private static int seriesRecordingState(String state) { - switch (state) { - case SeriesRecordings.STATE_SERIES_NORMAL: - return STATE_SERIES_NORMAL; - case SeriesRecordings.STATE_SERIES_STOPPED: - return STATE_SERIES_STOPPED; - } - return STATE_SERIES_NORMAL; - } - - /** - * Builder for {@link SeriesRecording}. - */ - public static class Builder { - private long mId = ID_NOT_SET; - private long mPriority = DvrScheduleManager.DEFAULT_SERIES_PRIORITY; - private String mTitle; - private String mDescription; - private String mLongDescription; - private String mInputId; - private long mChannelId; - private String mSeriesId; - private int mStartFromSeason = SeriesRecordings.THE_BEGINNING; - private int mStartFromEpisode = SeriesRecordings.THE_BEGINNING; - private int mChannelOption = OPTION_CHANNEL_ONE; - private int[] mCanonicalGenreIds; - private String mPosterUri; - private String mPhotoUri; - private int mState = SeriesRecording.STATE_SERIES_NORMAL; - - /** - * @see #getId() - */ - public Builder setId(long id) { - mId = id; - return this; - } - - /** - * @see #getPriority() () - */ - public Builder setPriority(long priority) { - mPriority = priority; - return this; - } - - /** - * @see #getTitle() - */ - public Builder setTitle(String title) { - mTitle = title; - return this; - } - - /** - * @see #getDescription() - */ - public Builder setDescription(String description) { - mDescription = description; - return this; - } - - /** - * @see #getLongDescription() - */ - public Builder setLongDescription(String longDescription) { - mLongDescription = longDescription; - return this; - } - - /** - * @see #getInputId() - */ - public Builder setInputId(String inputId) { - mInputId = inputId; - return this; - } - - /** - * @see #getChannelId() - */ - public Builder setChannelId(long channelId) { - mChannelId = channelId; - return this; - } - - /** - * @see #getSeriesId() - */ - public Builder setSeriesId(String seriesId) { - mSeriesId = seriesId; - return this; - } - - /** - * @see #getStartFromSeason() - */ - public Builder setStartFromSeason(int startFromSeason) { - mStartFromSeason = startFromSeason; - return this; - } - - /** - * @see #getChannelOption() - */ - public Builder setChannelOption(@ChannelOption int option) { - mChannelOption = option; - return this; - } - - /** - * @see #getStartFromEpisode() - */ - public Builder setStartFromEpisode(int startFromEpisode) { - mStartFromEpisode = startFromEpisode; - return this; - } - - /** - * @see #getCanonicalGenreIds() - */ - public Builder setCanonicalGenreIds(String genres) { - mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres); - return this; - } - - /** - * @see #getCanonicalGenreIds() - */ - public Builder setCanonicalGenreIds(int[] canonicalGenreIds) { - mCanonicalGenreIds = canonicalGenreIds; - return this; - } - - /** - * @see #getPosterUri() - */ - public Builder setPosterUri(String posterUri) { - mPosterUri = posterUri; - return this; - } - - /** - * @see #getPhotoUri() - */ - public Builder setPhotoUri(String photoUri) { - mPhotoUri = photoUri; - return this; - } - - /** - * @see #getState() - */ - public Builder setState(@SeriesState int state) { - mState = state; - return this; - } - - /** - * Creates a new {@link SeriesRecording}. - */ - public SeriesRecording build() { - return new SeriesRecording(mId, mPriority, mTitle, mDescription, mLongDescription, - mInputId, mChannelId, mSeriesId, mStartFromSeason, mStartFromEpisode, - mChannelOption, mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); - } - } - - public static SeriesRecording fromParcel(Parcel in) { - return new Builder() - .setId(in.readLong()) - .setPriority(in.readLong()) - .setTitle(in.readString()) - .setDescription(in.readString()) - .setLongDescription(in.readString()) - .setInputId(in.readString()) - .setChannelId(in.readLong()) - .setSeriesId(in.readString()) - .setStartFromSeason(in.readInt()) - .setStartFromEpisode(in.readInt()) - .setChannelOption(in.readInt()) - .setCanonicalGenreIds(in.createIntArray()) - .setPosterUri(in.readString()) - .setPhotoUri(in.readString()) - .setState(in.readInt()) - .build(); - } - - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - @Override - public SeriesRecording createFromParcel(Parcel in) { - return SeriesRecording.fromParcel(in); - } - - @Override - public SeriesRecording[] newArray(int size) { - return new SeriesRecording[size]; - } - }; - - private long mId; - private final long mPriority; - private final String mTitle; - private final String mDescription; - private final String mLongDescription; - private final String mInputId; - private final long mChannelId; - private final String mSeriesId; - private final int mStartFromSeason; - private final int mStartFromEpisode; - @ChannelOption private final int mChannelOption; - private final int[] mCanonicalGenreIds; - private final String mPosterUri; - private final String mPhotoUri; - @SeriesState private int mState; - - /** - * The input id of this SeriesRecording. - */ - public String getInputId() { - return mInputId; - } - - /** - * The channelId to match. The channel ID might not be valid when the channel option is "ALL". - */ - public long getChannelId() { - return mChannelId; - } - - /** - * The id of this SeriesRecording. - */ - public long getId() { - return mId; - } - - /** - * Sets the ID. - */ - public void setId(long id) { - mId = id; - } - - /** - * The priority of this recording. - * - *

The highest number is recorded first. If there is a tie in mPriority then the higher mId - * wins. - */ - public long getPriority() { - return mPriority; - } - - /** - * The series title. - */ - public String getTitle() { - return mTitle; - } - - /** - * The series description. - */ - public String getDescription() { - return mDescription; - } - - /** - * The long series description. - */ - public String getLongDescription() { - return mLongDescription; - } - - /** - * SeriesId when not null is used to match programs instead of using title and channelId. - * - *

SeriesId is an opaque but stable string. - */ - public String getSeriesId() { - return mSeriesId; - } - - /** - * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then - * only record episodes with a episodeNumber >= this - */ - public int getStartFromEpisode() { - return mStartFromEpisode; - } - - /** - * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a - * seasonNumber >= this - */ - public int getStartFromSeason() { - return mStartFromSeason; - } - - /** - * Returns the channel recording option. - */ - @ChannelOption public int getChannelOption() { - return mChannelOption; - } - - /** - * Returns the canonical genre ID's. - */ - public int[] getCanonicalGenreIds() { - return mCanonicalGenreIds; - } - - /** - * Returns the poster URI. - */ - public String getPosterUri() { - return mPosterUri; - } - - /** - * Returns the photo URI. - */ - public String getPhotoUri() { - return mPhotoUri; - } - - /** - * Returns the state of series recording. - */ - @SeriesState public int getState() { - return mState; - } - - /** - * Checks whether the series recording is stopped or not. - */ - public boolean isStopped() { - return mState == STATE_SERIES_STOPPED; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof SeriesRecording)) return false; - SeriesRecording that = (SeriesRecording) o; - return mPriority == that.mPriority - && mChannelId == that.mChannelId - && mStartFromSeason == that.mStartFromSeason - && mStartFromEpisode == that.mStartFromEpisode - && Objects.equals(mId, that.mId) - && Objects.equals(mTitle, that.mTitle) - && Objects.equals(mDescription, that.mDescription) - && Objects.equals(mLongDescription, that.mLongDescription) - && Objects.equals(mSeriesId, that.mSeriesId) - && mChannelOption == that.mChannelOption - && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds) - && Objects.equals(mPosterUri, that.mPosterUri) - && Objects.equals(mPhotoUri, that.mPhotoUri) - && mState == that.mState; - } - - @Override - public int hashCode() { - return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId, - mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption, - mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); - } - - @Override - public String toString() { - return "SeriesRecording{" + - "inputId=" + mInputId + - ", channelId=" + mChannelId + - ", id='" + mId + '\'' + - ", priority=" + mPriority + - ", title='" + mTitle + '\'' + - ", description='" + mDescription + '\'' + - ", longDescription='" + mLongDescription + '\'' + - ", startFromSeason=" + mStartFromSeason + - ", startFromEpisode=" + mStartFromEpisode + - ", channelOption=" + mChannelOption + - ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) + - ", posterUri=" + mPosterUri + - ", photoUri=" + mPhotoUri + - ", state=" + mState + - '}'; - } - - private SeriesRecording(long id, long priority, String title, String description, - String longDescription, String inputId, long channelId, String seriesId, - int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds, - String posterUri, String photoUri, int state) { - this.mId = id; - this.mPriority = priority; - this.mTitle = title; - this.mDescription = description; - this.mLongDescription = longDescription; - this.mInputId = inputId; - this.mChannelId = channelId; - this.mSeriesId = seriesId; - this.mStartFromSeason = startFromSeason; - this.mStartFromEpisode = startFromEpisode; - this.mChannelOption = channelOption; - this.mCanonicalGenreIds = canonicalGenreIds; - this.mPosterUri = posterUri; - this.mPhotoUri = photoUri; - this.mState = state; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int paramInt) { - out.writeLong(mId); - out.writeLong(mPriority); - out.writeString(mTitle); - out.writeString(mDescription); - out.writeString(mLongDescription); - out.writeString(mInputId); - out.writeLong(mChannelId); - out.writeString(mSeriesId); - out.writeInt(mStartFromSeason); - out.writeInt(mStartFromEpisode); - out.writeInt(mChannelOption); - out.writeIntArray(mCanonicalGenreIds); - out.writeString(mPosterUri); - out.writeString(mPhotoUri); - out.writeInt(mState); - } - - /** - * Returns an array containing all of the elements in the list. - */ - public static SeriesRecording[] toArray(Collection series) { - return series.toArray(new SeriesRecording[series.size()]); - } - - /** - * Returns {@code true} if the {@code program} is part of the series and meets the season and - * episode constraints. - */ - public boolean matchProgram(Program program) { - return matchProgram(program, mChannelOption); - } - - /** - * Returns {@code true} if the {@code program} is part of the series and meets the season and - * episode constraints. It checks the channel option only if {@code checkChannelOption} is - * {@code true}. - */ - public boolean matchProgram(Program program, @ChannelOption int channelOption) { - String seriesId = program.getSeriesId(); - long channelId = program.getChannelId(); - String seasonNumber = program.getSeasonNumber(); - String episodeNumber = program.getEpisodeNumber(); - if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE - && mChannelId != channelId)) { - return false; - } - // Season number and episode number matches if - // start_season_number < program_season_number - // || (start_season_number == program_season_number - // && start_episode_number <= program_episode_number). - if (mStartFromSeason == SeriesRecordings.THE_BEGINNING - || TextUtils.isEmpty(seasonNumber)) { - return true; - } else { - int intSeasonNumber; - try { - intSeasonNumber = Integer.valueOf(seasonNumber); - } catch (NumberFormatException e) { - return true; - } - if (intSeasonNumber > mStartFromSeason) { - return true; - } else if (intSeasonNumber < mStartFromSeason) { - return false; - } - } - if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING - || TextUtils.isEmpty(episodeNumber)) { - return true; - } else { - int intEpisodeNumber; - try { - intEpisodeNumber = Integer.valueOf(episodeNumber); - } catch (NumberFormatException e) { - return true; - } - return intEpisodeNumber >= mStartFromEpisode; - } - } -} diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java deleted file mode 100644 index 5ed12ce8..00000000 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ /dev/null @@ -1,579 +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; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.Context; -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; -import android.util.LongSparseArray; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.common.CollectionUtils; -import com.android.tv.common.SharedPreferencesUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.Program; -import com.android.tv.data.epg.EpgFetcher; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; -import com.android.tv.experiments.Experiments; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.Set; - -/** - * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}. - *

- * The current implementation assumes that the series recordings are scheduled only for one channel. - */ -@TargetApi(Build.VERSION_CODES.N) -public class SeriesRecordingScheduler { - private static final String TAG = "SeriesRecordingSchd"; - private static final boolean DEBUG = false; - - private static final String KEY_FETCHED_SERIES_IDS = - "SeriesRecordingScheduler.fetched_series_ids"; - - @SuppressLint("StaticFieldLeak") - private static SeriesRecordingScheduler sInstance; - - /** - * Creates and returns the {@link SeriesRecordingScheduler}. - */ - public static synchronized SeriesRecordingScheduler getInstance(Context context) { - if (sInstance == null) { - sInstance = new SeriesRecordingScheduler(context); - } - return sInstance; - } - - private final Context mContext; - private final DvrManager mDvrManager; - private final WritableDvrDataManager mDataManager; - private final List mScheduleTasks = new ArrayList<>(); - private final List mFetchSeriesInfoTasks = new ArrayList<>(); - private final Set mFetchedSeriesIds = new ArraySet<>(); - private final SharedPreferences mSharedPreferences; - private boolean mStarted; - private boolean mPaused; - private final Set mPendingSeriesRecordings = new ArraySet<>(); - private final Set mOnSeriesRecordingUpdatedListeners = - new CopyOnWriteArraySet<>(); - - - private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - executeFetchSeriesInfoTask(seriesRecording); - } - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - // Cancel the update. - for (Iterator iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { - SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, - SeriesRecording.ID_COMPARATOR).isEmpty()) { - task.cancel(true); - iter.remove(); - } - } - } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - List stopped = new ArrayList<>(); - List normal = new ArrayList<>(); - for (SeriesRecording r : seriesRecordings) { - if (r.isStopped()) { - stopped.add(r); - } else { - normal.add(r); - } - } - if (!stopped.isEmpty()) { - onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); - } - if (!normal.isEmpty()) { - updateSchedules(normal); - } - } - }; - - private final ScheduledRecordingListener mScheduledRecordingListener = - new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... schedules) { - // No need to update series recordings when the new schedule is added. - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { - handleScheduledRecordingChange(Arrays.asList(schedules)); - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { - List schedulesForUpdate = new ArrayList<>(); - for (ScheduledRecording r : schedules) { - if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED - || r.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED) - && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET - && !TextUtils.isEmpty(r.getSeasonNumber()) - && !TextUtils.isEmpty(r.getEpisodeNumber())) { - schedulesForUpdate.add(r); - } - } - if (!schedulesForUpdate.isEmpty()) { - handleScheduledRecordingChange(schedulesForUpdate); - } - } - - private void handleScheduledRecordingChange(List schedules) { - if (schedules.isEmpty()) { - return; - } - Set seriesRecordingIds = new HashSet<>(); - for (ScheduledRecording r : schedules) { - if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { - seriesRecordingIds.add(r.getSeriesRecordingId()); - } - } - if (!seriesRecordingIds.isEmpty()) { - List seriesRecordings = new ArrayList<>(); - for (Long id : seriesRecordingIds) { - SeriesRecording seriesRecording = mDataManager.getSeriesRecording(id); - if (seriesRecording != null) { - seriesRecordings.add(seriesRecording); - } - } - if (!seriesRecordings.isEmpty()) { - updateSchedules(seriesRecordings); - } - } - } - }; - - private SeriesRecordingScheduler(Context context) { - mContext = context.getApplicationContext(); - ApplicationSingletons appSingletons = TvApplication.getSingletons(context); - mDvrManager = appSingletons.getDvrManager(); - mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); - mSharedPreferences = context.getSharedPreferences( - SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); - mFetchedSeriesIds.addAll(mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, - Collections.emptySet())); - } - - /** - * Starts the scheduler. - */ - @MainThread - public void start() { - SoftPreconditions.checkState(mDataManager.isInitialized()); - if (mStarted) { - return; - } - if (DEBUG) Log.d(TAG, "start"); - mStarted = true; - mDataManager.addSeriesRecordingListener(mSeriesRecordingListener); - mDataManager.addScheduledRecordingListener(mScheduledRecordingListener); - startFetchingSeriesInfo(); - updateSchedules(mDataManager.getSeriesRecordings()); - } - - @MainThread - public void stop() { - if (!mStarted) { - return; - } - if (DEBUG) Log.d(TAG, "stop"); - mStarted = false; - for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { - task.cancel(true); - } - mFetchSeriesInfoTasks.clear(); - for (SeriesRecordingUpdateTask task : mScheduleTasks) { - task.cancel(true); - } - mScheduleTasks.clear(); - mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); - mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); - } - - private void startFetchingSeriesInfo() { - for (SeriesRecording seriesRecording : mDataManager.getSeriesRecordings()) { - if (!mFetchedSeriesIds.contains(seriesRecording.getSeriesId())) { - executeFetchSeriesInfoTask(seriesRecording); - } - } - } - - private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) { - if (Experiments.CLOUD_EPG.get()) { - FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); - task.execute(); - mFetchSeriesInfoTasks.add(task); - } - } - - /** - * Pauses the updates of the series recordings. - */ - public void pauseUpdate() { - if (DEBUG) Log.d(TAG, "Schedule paused"); - if (mPaused) { - return; - } - mPaused = true; - if (!mStarted) { - return; - } - for (SeriesRecordingUpdateTask task : mScheduleTasks) { - for (SeriesRecording r : task.getSeriesRecordings()) { - mPendingSeriesRecordings.add(r.getId()); - } - task.cancel(true); - } - } - - /** - * Resumes the updates of the series recordings. - */ - public void resumeUpdate() { - if (DEBUG) Log.d(TAG, "Schedule resumed"); - if (!mPaused) { - return; - } - mPaused = false; - if (!mStarted) { - return; - } - if (!mPendingSeriesRecordings.isEmpty()) { - List seriesRecordings = new ArrayList<>(); - for (long seriesRecordingId : mPendingSeriesRecordings) { - SeriesRecording seriesRecording = - mDataManager.getSeriesRecording(seriesRecordingId); - if (seriesRecording != null) { - seriesRecordings.add(seriesRecording); - } - } - if (!seriesRecordings.isEmpty()) { - updateSchedules(seriesRecordings); - } - } - } - - /** - * Update schedules for the given series recordings. If it's paused, the update will be done - * after it's resumed. - */ - public void updateSchedules(Collection seriesRecordings) { - if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings); - if (!mStarted) { - if (DEBUG) Log.d(TAG, "Not started yet."); - return; - } - if (mPaused) { - for (SeriesRecording r : seriesRecordings) { - mPendingSeriesRecordings.add(r.getId()); - } - if (DEBUG) { - Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size=" - + mPendingSeriesRecordings.size()); - } - return; - } - Set previousSeriesRecordings = new HashSet<>(); - for (Iterator iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { - SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings, - SeriesRecording.ID_COMPARATOR)) { - // The task is affected by the seriesRecordings - task.cancel(true); - previousSeriesRecordings.addAll(task.getSeriesRecordings()); - iter.remove(); - } - } - List seriesRecordingsToUpdate = CollectionUtils.union(seriesRecordings, - previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); - for (Iterator iter = seriesRecordingsToUpdate.iterator(); - iter.hasNext(); ) { - SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); - if (seriesRecording == null || seriesRecording.isStopped()) { - // Series recording has been removed or stopped. - iter.remove(); - } - } - if (seriesRecordingsToUpdate.isEmpty()) { - return; - } - if (needToReadAllChannels(seriesRecordingsToUpdate)) { - SeriesRecordingUpdateTask task = - new SeriesRecordingUpdateTask(seriesRecordingsToUpdate); - mScheduleTasks.add(task); - if (DEBUG) Log.d(TAG, "Added schedule task: " + task); - task.execute(); - } else { - for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { - SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask( - Collections.singletonList(seriesRecording)); - mScheduleTasks.add(task); - if (DEBUG) Log.d(TAG, "Added schedule task: " + task); - task.execute(); - } - } - } - - /** - * 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 seriesRecordingsToUpdate) { - for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { - if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { - return true; - } - } - return false; - } - - /** - * Pick one program per an episode. - * - *

Note that the programs which has been already scheduled have the highest priority, and all - * of them are added even though they are the same episodes. That's because the schedules - * should be added to the series recording. - *

If there are no existing schedules for an episode, one program which starts earlier is - * picked. - */ - private LongSparseArray> pickOneProgramPerEpisode( - List seriesRecordings, List programs) { - return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); - } - - /** - * @see #pickOneProgramPerEpisode(List, List) - */ - @VisibleForTesting - static LongSparseArray> pickOneProgramPerEpisode( - DvrDataManager dataManager, List seriesRecordings, - List programs) { - // Initialize. - LongSparseArray> result = new LongSparseArray<>(); - Map seriesRecordingIds = new HashMap<>(); - for (SeriesRecording seriesRecording : seriesRecordings) { - result.put(seriesRecording.getId(), new ArrayList<>()); - seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); - } - // Group programs by the episode. - Map> programsForEpisodeMap = new HashMap<>(); - for (Program program : programs) { - long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); - if (TextUtils.isEmpty(program.getSeasonNumber()) - || TextUtils.isEmpty(program.getEpisodeNumber())) { - // Add all the programs if it doesn't have season number or episode number. - result.get(seriesRecordingId).add(program); - continue; - } - ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId, - program.getSeasonNumber(), program.getEpisodeNumber()); - List programsForEpisode = programsForEpisodeMap.get(episode); - if (programsForEpisode == null) { - programsForEpisode = new ArrayList<>(); - programsForEpisodeMap.put(episode, programsForEpisode); - } - programsForEpisode.add(program); - } - // Pick one program. - for (Entry> entry : programsForEpisodeMap.entrySet()) { - List programsForEpisode = entry.getValue(); - Collections.sort(programsForEpisode, new Comparator() { - @Override - public int compare(Program lhs, Program rhs) { - // Place the existing schedule first. - boolean lhsScheduled = isProgramScheduled(dataManager, lhs); - boolean rhsScheduled = isProgramScheduled(dataManager, rhs); - if (lhsScheduled && !rhsScheduled) { - return -1; - } - if (!lhsScheduled && rhsScheduled) { - return 1; - } - // Sort by the start time in ascending order. - return lhs.compareTo(rhs); - } - }); - boolean added = false; - // Add all the scheduled programs - List programsForSeries = result.get(entry.getKey().seriesRecordingId); - for (Program program : programsForEpisode) { - if (isProgramScheduled(dataManager, program)) { - programsForSeries.add(program); - added = true; - } else if (!added) { - programsForSeries.add(program); - break; - } - } - } - return result; - } - - private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) { - ScheduledRecording schedule = - dataManager.getScheduledRecordingForProgramId(program.getId()); - return schedule != null && schedule.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED; - } - - private void updateFetchedSeries() { - mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply(); - } - - /** - * This works only for the existing series recordings. Do not use this task for the - * "adding series recording" UI. - */ - private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { - SeriesRecordingUpdateTask(List seriesRecordings) { - super(mContext, seriesRecordings); - } - - @Override - protected void onPostExecute(List programs) { - if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); - mScheduleTasks.remove(this); - if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " - + getSeriesRecordings()); - return; - } - LongSparseArray> seriesProgramMap = pickOneProgramPerEpisode( - getSeriesRecordings(), programs); - for (SeriesRecording seriesRecording : getSeriesRecordings()) { - // Check the series recording is still valid. - SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( - seriesRecording.getId()); - if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { - continue; - } - List programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); - if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null - && !programsToSchedule.isEmpty()) { - mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); - } - } - if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { - for (OnSeriesRecordingUpdatedListener listener - : mOnSeriesRecordingUpdatedListeners) { - listener.onSeriesRecordingUpdated( - SeriesRecording.toArray(getSeriesRecordings())); - } - } - } - - @Override - protected void onCancelled(List programs) { - mScheduleTasks.remove(this); - } - - @Override - public String toString() { - return "SeriesRecordingUpdateTask:{" - + "series_recordings=" + getSeriesRecordings() - + "}"; - } - } - - private class FetchSeriesInfoTask extends AsyncTask { - private SeriesRecording mSeriesRecording; - - FetchSeriesInfoTask(SeriesRecording seriesRecording) { - mSeriesRecording = seriesRecording; - } - - @Override - protected SeriesInfo doInBackground(Void... voids) { - return EpgFetcher.createEpgReader(mContext) - .getSeriesInfo(mSeriesRecording.getSeriesId()); - } - - @Override - protected void onPostExecute(SeriesInfo seriesInfo) { - if (seriesInfo != null) { - mDataManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) - .setTitle(seriesInfo.getTitle()) - .setDescription(seriesInfo.getDescription()) - .setLongDescription(seriesInfo.getLongDescription()) - .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) - .setPosterUri(seriesInfo.getPosterUri()) - .setPhotoUri(seriesInfo.getPhotoUri()) - .build()); - mFetchedSeriesIds.add(seriesInfo.getId()); - updateFetchedSeries(); - } - mFetchSeriesInfoTasks.remove(this); - } - - @Override - protected void onCancelled(SeriesInfo seriesInfo) { - mFetchSeriesInfoTasks.remove(this); - } - } - - /** - * A listener to notify when series recording are updated. - */ - public interface OnSeriesRecordingUpdatedListener { - void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); - } -} diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 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/data/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java new file mode 100644 index 00000000..2ade1dad --- /dev/null +++ b/src/com/android/tv/dvr/data/IdGenerator.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.data; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A class which generate the ID which increases sequentially. + */ +public class IdGenerator { + /** + * ID generator for the scheduled recording. + */ + public static final IdGenerator SCHEDULED_RECORDING = new IdGenerator(); + + /** + * ID generator for the series recording. + */ + public static final IdGenerator SERIES_RECORDING = new IdGenerator(); + + private final AtomicLong mMaxId = new AtomicLong(0); + + /** + * Sets the new maximum ID. + */ + public void setMaxId(long maxId) { + mMaxId.set(maxId); + } + + /** + * Returns the new ID which is greater than the existing maximum ID by 1. + */ + public long newId() { + return mMaxId.incrementAndGet(); + } +} diff --git a/src/com/android/tv/dvr/data/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java new file mode 100644 index 00000000..18e1c769 --- /dev/null +++ b/src/com/android/tv/dvr/data/RecordedProgram.java @@ -0,0 +1,868 @@ +/* + * 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 static android.media.tv.TvContract.RecordedPrograms; + +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.common.R; +import com.android.tv.data.BaseProgram; +import com.android.tv.data.GenreItems; +import com.android.tv.data.InternalDataUtils; +import com.android.tv.util.Utils; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. + */ +@TargetApi(Build.VERSION_CODES.N) +public class RecordedProgram extends BaseProgram { + public static final int ID_NOT_SET = -1; + + public final static String[] PROJECTION = { + // These are in exactly the order listed in RecordedPrograms + RecordedPrograms._ID, + RecordedPrograms.COLUMN_PACKAGE_NAME, + RecordedPrograms.COLUMN_INPUT_ID, + RecordedPrograms.COLUMN_CHANNEL_ID, + RecordedPrograms.COLUMN_TITLE, + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_SEASON_TITLE, + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, + RecordedPrograms.COLUMN_EPISODE_TITLE, + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_BROADCAST_GENRE, + RecordedPrograms.COLUMN_CANONICAL_GENRE, + RecordedPrograms.COLUMN_SHORT_DESCRIPTION, + RecordedPrograms.COLUMN_LONG_DESCRIPTION, + RecordedPrograms.COLUMN_VIDEO_WIDTH, + RecordedPrograms.COLUMN_VIDEO_HEIGHT, + RecordedPrograms.COLUMN_AUDIO_LANGUAGE, + RecordedPrograms.COLUMN_CONTENT_RATING, + RecordedPrograms.COLUMN_POSTER_ART_URI, + RecordedPrograms.COLUMN_THUMBNAIL_URI, + RecordedPrograms.COLUMN_SEARCHABLE, + RecordedPrograms.COLUMN_RECORDING_DATA_URI, + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + RecordedPrograms.COLUMN_VERSION_NUMBER, + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + }; + + public static RecordedProgram fromCursor(Cursor cursor) { + int index = 0; + Builder builder = builder() + .setId(cursor.getLong(index++)) + .setPackageName(cursor.getString(index++)) + .setInputId(cursor.getString(index++)) + .setChannelId(cursor.getLong(index++)) + .setTitle(cursor.getString(index++)) + .setSeasonNumber(cursor.getString(index++)) + .setSeasonTitle(cursor.getString(index++)) + .setEpisodeNumber(cursor.getString(index++)) + .setEpisodeTitle(cursor.getString(index++)) + .setStartTimeUtcMillis(cursor.getLong(index++)) + .setEndTimeUtcMillis(cursor.getLong(index++)) + .setBroadcastGenres(cursor.getString(index++)) + .setCanonicalGenres(cursor.getString(index++)) + .setShortDescription(cursor.getString(index++)) + .setLongDescription(cursor.getString(index++)) + .setVideoWidth(cursor.getInt(index++)) + .setVideoHeight(cursor.getInt(index++)) + .setAudioLanguage(cursor.getString(index++)) + .setContentRating(cursor.getString(index++)) + .setPosterArtUri(cursor.getString(index++)) + .setThumbnailUri(cursor.getString(index++)) + .setSearchable(cursor.getInt(index++) == 1) + .setDataUri(cursor.getString(index++)) + .setDataBytes(cursor.getLong(index++)) + .setDurationMillis(cursor.getLong(index++)) + .setExpireTimeUtcMillis(cursor.getLong(index++)) + .setInternalProviderFlag1(cursor.getInt(index++)) + .setInternalProviderFlag2(cursor.getInt(index++)) + .setInternalProviderFlag3(cursor.getInt(index++)) + .setInternalProviderFlag4(cursor.getInt(index++)) + .setVersionNumber(cursor.getInt(index++)); + if (Utils.isInBundledPackageSet(builder.mPackageName)) { + InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); + } + return builder.build(); + } + + public static ContentValues toValues(RecordedProgram recordedProgram) { + ContentValues values = new ContentValues(); + if (recordedProgram.mId != ID_NOT_SET) { + values.put(RecordedPrograms._ID, recordedProgram.mId); + } + values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.mInputId); + values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.mChannelId); + values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.mTitle); + values.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.mSeasonNumber); + values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.mSeasonTitle); + values.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.mEpisodeNumber); + values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.mTitle); + values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, + recordedProgram.mStartTimeUtcMillis); + values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.mEndTimeUtcMillis); + values.put(RecordedPrograms.COLUMN_BROADCAST_GENRE, + safeEncode(recordedProgram.mBroadcastGenres)); + values.put(RecordedPrograms.COLUMN_CANONICAL_GENRE, + safeEncode(recordedProgram.mCanonicalGenres)); + values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.mShortDescription); + values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.mLongDescription); + if (recordedProgram.mVideoWidth == 0) { + values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH); + } else { + values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.mVideoWidth); + } + if (recordedProgram.mVideoHeight == 0) { + values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT); + } else { + values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); + } + values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); + values.put(RecordedPrograms.COLUMN_CONTENT_RATING, recordedProgram.mContentRating); + values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri); + values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri); + values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0); + values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, + safeToString(recordedProgram.mDataUri)); + values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.mDataBytes); + values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, + recordedProgram.mDurationMillis); + values.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS, + recordedProgram.mExpireTimeUtcMillis); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA, + InternalDataUtils.serializeInternalProviderData(recordedProgram)); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1, + recordedProgram.mInternalProviderFlag1); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2, + recordedProgram.mInternalProviderFlag2); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3, + recordedProgram.mInternalProviderFlag3); + values.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4, + recordedProgram.mInternalProviderFlag4); + values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.mVersionNumber); + return values; + } + + public static class Builder{ + private long mId = ID_NOT_SET; + private String mPackageName; + private String mInputId; + private long mChannelId; + private String mTitle; + private String mSeriesId; + private String mSeasonNumber; + private String mSeasonTitle; + private String mEpisodeNumber; + private String mEpisodeTitle; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private String[] mBroadcastGenres; + private String[] mCanonicalGenres; + private String mShortDescription; + private String mLongDescription; + private int mVideoWidth; + private int mVideoHeight; + private String mAudioLanguage; + private String mContentRating; + private String mPosterArtUri; + private String mThumbnailUri; + private boolean mSearchable = true; + private Uri mDataUri; + private long mDataBytes; + private long mDurationMillis; + private long mExpireTimeUtcMillis; + private int mInternalProviderFlag1; + private int mInternalProviderFlag2; + private int mInternalProviderFlag3; + private int mInternalProviderFlag4; + private int mVersionNumber; + + public Builder setId(long id) { + mId = id; + return this; + } + + public Builder setPackageName(String packageName) { + mPackageName = packageName; + return this; + } + + public Builder setInputId(String inputId) { + mInputId = inputId; + return this; + } + + public Builder setChannelId(long channelId) { + mChannelId = channelId; + return this; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setSeriesId(String seriesId) { + mSeriesId = seriesId; + return this; + } + + public Builder setSeasonNumber(String seasonNumber) { + mSeasonNumber = seasonNumber; + return this; + } + + public Builder setSeasonTitle(String seasonTitle) { + mSeasonTitle = seasonTitle; + return this; + } + + public Builder setEpisodeNumber(String episodeNumber) { + mEpisodeNumber = episodeNumber; + return this; + } + + public Builder setEpisodeTitle(String episodeTitle) { + mEpisodeTitle = episodeTitle; + return this; + } + + public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { + mStartTimeUtcMillis = startTimeUtcMillis; + return this; + } + + public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { + mEndTimeUtcMillis = endTimeUtcMillis; + return this; + } + + public Builder setBroadcastGenres(String broadcastGenres) { + if (TextUtils.isEmpty(broadcastGenres)) { + mBroadcastGenres = null; + return this; + } + return setBroadcastGenres(TvContract.Programs.Genres.decode(broadcastGenres)); + } + + private Builder setBroadcastGenres(String[] broadcastGenres) { + mBroadcastGenres = broadcastGenres; + return this; + } + + public Builder setCanonicalGenres(String canonicalGenres) { + if (TextUtils.isEmpty(canonicalGenres)) { + mCanonicalGenres = null; + return this; + } + return setCanonicalGenres(TvContract.Programs.Genres.decode(canonicalGenres)); + } + + private Builder setCanonicalGenres(String[] canonicalGenres) { + mCanonicalGenres = canonicalGenres; + return this; + } + + public Builder setShortDescription(String shortDescription) { + mShortDescription = shortDescription; + return this; + } + + public Builder setLongDescription(String longDescription) { + mLongDescription = longDescription; + return this; + } + + public Builder setVideoWidth(int videoWidth) { + mVideoWidth = videoWidth; + return this; + } + + public Builder setVideoHeight(int videoHeight) { + mVideoHeight = videoHeight; + return this; + } + + public Builder setAudioLanguage(String audioLanguage) { + mAudioLanguage = audioLanguage; + return this; + } + + public Builder setContentRating(String contentRating) { + mContentRating = contentRating; + return this; + } + + private Uri toUri(String uriString) { + try { + return uriString == null ? null : Uri.parse(uriString); + } catch (Exception e) { + return null; + } + } + + public Builder setPosterArtUri(String posterArtUri) { + mPosterArtUri = posterArtUri; + return this; + } + + public Builder setThumbnailUri(String thumbnailUri) { + mThumbnailUri = thumbnailUri; + return this; + } + + public Builder setSearchable(boolean searchable) { + mSearchable = searchable; + return this; + } + + public Builder setDataUri(String dataUri) { + return setDataUri(toUri(dataUri)); + } + + public Builder setDataUri(Uri dataUri) { + mDataUri = dataUri; + return this; + } + + public Builder setDataBytes(long dataBytes) { + mDataBytes = dataBytes; + return this; + } + + public Builder setDurationMillis(long durationMillis) { + mDurationMillis = durationMillis; + return this; + } + + public Builder setExpireTimeUtcMillis(long expireTimeUtcMillis) { + mExpireTimeUtcMillis = expireTimeUtcMillis; + return this; + } + + public Builder setInternalProviderFlag1(int internalProviderFlag1) { + mInternalProviderFlag1 = internalProviderFlag1; + return this; + } + + public Builder setInternalProviderFlag2(int internalProviderFlag2) { + mInternalProviderFlag2 = internalProviderFlag2; + return this; + } + + public Builder setInternalProviderFlag3(int internalProviderFlag3) { + mInternalProviderFlag3 = internalProviderFlag3; + return this; + } + + public Builder setInternalProviderFlag4(int internalProviderFlag4) { + mInternalProviderFlag4 = internalProviderFlag4; + return this; + } + + public Builder setVersionNumber(int versionNumber) { + mVersionNumber = versionNumber; + return this; + } + + public RecordedProgram build() { + // Generate the series ID for the episodic program of other TV input. + if (TextUtils.isEmpty(mSeriesId) + && !TextUtils.isEmpty(mEpisodeNumber)) { + setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle)); + } + return new RecordedProgram(mId, mPackageName, mInputId, mChannelId, mTitle, mSeriesId, + mSeasonNumber, mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis, + mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription, + mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRating, + mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes, + mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1, + mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4, + mVersionNumber); + } + } + + public static Builder builder() { return new Builder(); } + + public static Builder buildFrom(RecordedProgram orig) { + return builder() + .setId(orig.getId()) + .setPackageName(orig.getPackageName()) + .setInputId(orig.getInputId()) + .setChannelId(orig.getChannelId()) + .setTitle(orig.getTitle()) + .setSeriesId(orig.getSeriesId()) + .setSeasonNumber(orig.getSeasonNumber()) + .setSeasonTitle(orig.getSeasonTitle()) + .setEpisodeNumber(orig.getEpisodeNumber()) + .setEpisodeTitle(orig.getEpisodeTitle()) + .setStartTimeUtcMillis(orig.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(orig.getEndTimeUtcMillis()) + .setBroadcastGenres(orig.getBroadcastGenres()) + .setCanonicalGenres(orig.getCanonicalGenres()) + .setShortDescription(orig.getDescription()) + .setLongDescription(orig.getLongDescription()) + .setVideoWidth(orig.getVideoWidth()) + .setVideoHeight(orig.getVideoHeight()) + .setAudioLanguage(orig.getAudioLanguage()) + .setContentRating(orig.getContentRating()) + .setPosterArtUri(orig.getPosterArtUri()) + .setThumbnailUri(orig.getThumbnailUri()) + .setSearchable(orig.isSearchable()) + .setInternalProviderFlag1(orig.getInternalProviderFlag1()) + .setInternalProviderFlag2(orig.getInternalProviderFlag2()) + .setInternalProviderFlag3(orig.getInternalProviderFlag3()) + .setInternalProviderFlag4(orig.getInternalProviderFlag4()) + .setVersionNumber(orig.getVersionNumber()); + } + + public static final Comparator START_TIME_THEN_ID_COMPARATOR = + new Comparator() { + @Override + public int compare(RecordedProgram lhs, RecordedProgram rhs) { + int res = + Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis()); + if (res != 0) { + return res; + } + return Long.compare(lhs.mId, rhs.mId); + } + }; + + private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); + + private final long mId; + private final String mPackageName; + private final String mInputId; + private final long mChannelId; + private final String mTitle; + private final String mSeriesId; + private final String mSeasonNumber; + private final String mSeasonTitle; + private final String mEpisodeNumber; + private final String mEpisodeTitle; + private final long mStartTimeUtcMillis; + private final long mEndTimeUtcMillis; + private final String[] mBroadcastGenres; + private final String[] mCanonicalGenres; + private final String mShortDescription; + private final String mLongDescription; + private final int mVideoWidth; + private final int mVideoHeight; + private final String mAudioLanguage; + private final String mContentRating; + private final String mPosterArtUri; + private final String mThumbnailUri; + private final boolean mSearchable; + private final Uri mDataUri; + private final long mDataBytes; + private final long mDurationMillis; + private final long mExpireTimeUtcMillis; + private final int mInternalProviderFlag1; + private final int mInternalProviderFlag2; + private final int mInternalProviderFlag3; + private final int mInternalProviderFlag4; + private final int mVersionNumber; + + private RecordedProgram(long id, String packageName, String inputId, long channelId, + String title, String seriesId, String seasonNumber, String seasonTitle, + String episodeNumber, String episodeTitle, long startTimeUtcMillis, + long endTimeUtcMillis, String[] broadcastGenres, String[] canonicalGenres, + String shortDescription, String longDescription, int videoWidth, int videoHeight, + String audioLanguage, String contentRating, String posterArtUri, String thumbnailUri, + boolean searchable, Uri dataUri, long dataBytes, long durationMillis, + long expireTimeUtcMillis, int internalProviderFlag1, int internalProviderFlag2, + int internalProviderFlag3, int internalProviderFlag4, int versionNumber) { + mId = id; + mPackageName = packageName; + mInputId = inputId; + mChannelId = channelId; + mTitle = title; + mSeriesId = seriesId; + mSeasonNumber = seasonNumber; + mSeasonTitle = seasonTitle; + mEpisodeNumber = episodeNumber; + mEpisodeTitle = episodeTitle; + mStartTimeUtcMillis = startTimeUtcMillis; + mEndTimeUtcMillis = endTimeUtcMillis; + mBroadcastGenres = broadcastGenres; + mCanonicalGenres = canonicalGenres; + mShortDescription = shortDescription; + mLongDescription = longDescription; + mVideoWidth = videoWidth; + mVideoHeight = videoHeight; + + mAudioLanguage = audioLanguage; + mContentRating = contentRating; + mPosterArtUri = posterArtUri; + mThumbnailUri = thumbnailUri; + mSearchable = searchable; + mDataUri = dataUri; + mDataBytes = dataBytes; + mDurationMillis = durationMillis; + mExpireTimeUtcMillis = expireTimeUtcMillis; + mInternalProviderFlag1 = internalProviderFlag1; + mInternalProviderFlag2 = internalProviderFlag2; + mInternalProviderFlag3 = internalProviderFlag3; + mInternalProviderFlag4 = internalProviderFlag4; + mVersionNumber = versionNumber; + } + + public String getAudioLanguage() { + return mAudioLanguage; + } + + public String[] getBroadcastGenres() { + return mBroadcastGenres; + } + + public String[] getCanonicalGenres() { + return mCanonicalGenres; + } + + /** + * Returns array of canonical genre ID's for this recorded program. + */ + @Override + public int[] getCanonicalGenreIds() { + if (mCanonicalGenres == null) { + return null; + } + int[] genreIds = new int[mCanonicalGenres.length]; + for (int i = 0; i < mCanonicalGenres.length; i++) { + genreIds[i] = GenreItems.getId(mCanonicalGenres[i]); + } + return genreIds; + } + + @Override + public long getChannelId() { + return mChannelId; + } + + public String getContentRating() { + return mContentRating; + } + + public Uri getDataUri() { + return mDataUri; + } + + public long getDataBytes() { + return mDataBytes; + } + + @Override + public long getDurationMillis() { + return mDurationMillis; + } + + @Override + public long getEndTimeUtcMillis() { + return mEndTimeUtcMillis; + } + + @Override + public String getEpisodeNumber() { + return mEpisodeNumber; + } + + public String getEpisodeTitle() { + return mEpisodeTitle; + } + + @Override + public String getEpisodeDisplayTitle(Context context) { + if (!TextUtils.isEmpty(mEpisodeNumber)) { + String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; + if (TextUtils.equals(mSeasonNumber, "0")) { + // Do not show "S0: ". + return String.format(context.getResources().getString( + R.string.display_episode_title_format_no_season_number), + mEpisodeNumber, episodeTitle); + } else { + return String.format(context.getResources().getString( + R.string.display_episode_title_format), + mSeasonNumber, mEpisodeNumber, episodeTitle); + } + } + return mEpisodeTitle; + } + + @Nullable + @Override + public String getTitleWithEpisodeNumber(Context context) { + if (TextUtils.isEmpty(mTitle)) { + return mTitle; + } + if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { + return TextUtils.isEmpty(mEpisodeNumber) ? mTitle : context.getString( + R.string.program_title_with_episode_number_no_season, mTitle, mEpisodeNumber); + } else { + return context.getString(R.string.program_title_with_episode_number, mTitle, + mSeasonNumber, mEpisodeNumber); + } + } + + @Nullable + public String getEpisodeDisplayNumber(Context context) { + if (!TextUtils.isEmpty(mEpisodeNumber)) { + if (TextUtils.equals(mSeasonNumber, "0")) { + // Do not show "S0: ". + return String.format(context.getResources().getString( + R.string.display_episode_number_format_no_season_number), mEpisodeNumber); + } else { + return String.format(context.getResources().getString( + R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber); + } + } + return null; + } + + public long getExpireTimeUtcMillis() { + return mExpireTimeUtcMillis; + } + + public long getId() { + return mId; + } + + public String getPackageName() { + return mPackageName; + } + + public String getInputId() { + return mInputId; + } + + public int getInternalProviderFlag1() { + return mInternalProviderFlag1; + } + + public int getInternalProviderFlag2() { + return mInternalProviderFlag2; + } + + public int getInternalProviderFlag3() { + return mInternalProviderFlag3; + } + + public int getInternalProviderFlag4() { + return mInternalProviderFlag4; + } + + @Override + public String getDescription() { + return mShortDescription; + } + + @Override + public String getLongDescription() { + return mLongDescription; + } + + @Override + public String getPosterArtUri() { + return mPosterArtUri; + } + + @Override + public boolean isValid() { + return true; + } + + public boolean isSearchable() { + return mSearchable; + } + + @Override + public String getSeriesId() { + return mSeriesId; + } + + @Override + public String getSeasonNumber() { + return mSeasonNumber; + } + + public String getSeasonTitle() { + return mSeasonTitle; + } + + @Override + public long getStartTimeUtcMillis() { + return mStartTimeUtcMillis; + } + + @Override + public String getThumbnailUri() { + return mThumbnailUri; + } + + @Override + public String getTitle() { + return mTitle; + } + + public Uri getUri() { + return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, mId); + } + + public int getVersionNumber() { + return mVersionNumber; + } + + public int getVideoHeight() { + return mVideoHeight; + } + + public int getVideoWidth() { + return mVideoWidth; + } + + /** + * Checks whether the recording has been clipped or not. + */ + public boolean isClipped() { + return mEndTimeUtcMillis - mStartTimeUtcMillis - mDurationMillis > CLIPPED_THRESHOLD_MS; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordedProgram that = (RecordedProgram) o; + return Objects.equals(mId, that.mId) && + Objects.equals(mChannelId, that.mChannelId) && + Objects.equals(mSeriesId, that.mSeriesId) && + Objects.equals(mSeasonNumber, that.mSeasonNumber) && + Objects.equals(mSeasonTitle, that.mSeasonTitle) && + Objects.equals(mEpisodeNumber, that.mEpisodeNumber) && + Objects.equals(mStartTimeUtcMillis, that.mStartTimeUtcMillis) && + Objects.equals(mEndTimeUtcMillis, that.mEndTimeUtcMillis) && + Objects.equals(mVideoWidth, that.mVideoWidth) && + Objects.equals(mVideoHeight, that.mVideoHeight) && + Objects.equals(mSearchable, that.mSearchable) && + Objects.equals(mDataBytes, that.mDataBytes) && + Objects.equals(mDurationMillis, that.mDurationMillis) && + Objects.equals(mExpireTimeUtcMillis, that.mExpireTimeUtcMillis) && + Objects.equals(mInternalProviderFlag1, that.mInternalProviderFlag1) && + Objects.equals(mInternalProviderFlag2, that.mInternalProviderFlag2) && + Objects.equals(mInternalProviderFlag3, that.mInternalProviderFlag3) && + Objects.equals(mInternalProviderFlag4, that.mInternalProviderFlag4) && + Objects.equals(mVersionNumber, that.mVersionNumber) && + Objects.equals(mTitle, that.mTitle) && + Objects.equals(mEpisodeTitle, that.mEpisodeTitle) && + Arrays.equals(mBroadcastGenres, that.mBroadcastGenres) && + Arrays.equals(mCanonicalGenres, that.mCanonicalGenres) && + Objects.equals(mShortDescription, that.mShortDescription) && + Objects.equals(mLongDescription, that.mLongDescription) && + Objects.equals(mAudioLanguage, that.mAudioLanguage) && + Objects.equals(mContentRating, that.mContentRating) && + Objects.equals(mPosterArtUri, that.mPosterArtUri) && + Objects.equals(mThumbnailUri, that.mThumbnailUri); + } + + /** + * Hashes based on the ID. + */ + @Override + public int hashCode() { + return Objects.hash(mId); + } + + @Override + public String toString() { + return "RecordedProgram" + + "[" + mId + + "]{ mPackageName=" + mPackageName + + ", mInputId='" + mInputId + '\'' + + ", mChannelId='" + mChannelId + '\'' + + ", mTitle='" + mTitle + '\'' + + ", mSeriesId='" + mSeriesId + '\'' + + ", mEpisodeNumber=" + mEpisodeNumber + + ", mEpisodeTitle='" + mEpisodeTitle + '\'' + + ", mStartTimeUtcMillis=" + mStartTimeUtcMillis + + ", mEndTimeUtcMillis=" + mEndTimeUtcMillis + + ", mBroadcastGenres=" + + (mBroadcastGenres != null ? Arrays.toString(mBroadcastGenres) : "null") + + ", mCanonicalGenres=" + + (mCanonicalGenres != null ? Arrays.toString(mCanonicalGenres) : "null") + + ", mShortDescription='" + mShortDescription + '\'' + + ", mLongDescription='" + mLongDescription + '\'' + + ", mVideoHeight=" + mVideoHeight + + ", mVideoWidth=" + mVideoWidth + + ", mAudioLanguage='" + mAudioLanguage + '\'' + + ", mContentRating='" + mContentRating + '\'' + + ", mPosterArtUri=" + mPosterArtUri + + ", mThumbnailUri=" + mThumbnailUri + + ", mSearchable=" + mSearchable + + ", mDataUri=" + mDataUri + + ", mDataBytes=" + mDataBytes + + ", mDurationMillis=" + mDurationMillis + + ", mExpireTimeUtcMillis=" + mExpireTimeUtcMillis + + ", mInternalProviderFlag1=" + mInternalProviderFlag1 + + ", mInternalProviderFlag2=" + mInternalProviderFlag2 + + ", mInternalProviderFlag3=" + mInternalProviderFlag3 + + ", mInternalProviderFlag4=" + mInternalProviderFlag4 + + ", mSeasonNumber=" + mSeasonNumber + + ", mSeasonTitle=" + mSeasonTitle + + ", mVersionNumber=" + mVersionNumber + + '}'; + } + + @Nullable + private static String safeToString(@Nullable Object o) { + return o == null ? null : o.toString(); + } + + @Nullable + private static String safeEncode(@Nullable String[] genres) { + return genres == null ? null : TvContract.Programs.Genres.encode(genres); + } + + /** + * Returns an array containing all of the elements in the list. + */ + public static RecordedProgram[] toArray(Collection recordedPrograms) { + return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]); + } +} diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java new file mode 100644 index 00000000..88849a2c --- /dev/null +++ b/src/com/android/tv/dvr/data/ScheduledRecording.java @@ -0,0 +1,902 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Range; + +import com.android.tv.R; +import com.android.tv.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; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +/** + * A data class for one recording contents. + */ +@VisibleForTesting +public final class ScheduledRecording implements Parcelable { + private static final String TAG = "ScheduledRecording"; + + /** + * Indicates that the ID is not assigned yet. + */ + public static final long ID_NOT_SET = 0; + + /** + * The default priority of the recording. + */ + public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; + + /** + * Compares the start time in ascending order. + */ + public static final Comparator START_TIME_COMPARATOR + = new Comparator() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs); + } + }; + + /** + * Compares the end time in ascending order. + */ + public static final Comparator END_TIME_COMPARATOR + = new Comparator() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs); + } + }; + + /** + * Compares ID in ascending order. The schedule with the larger ID was created later. + */ + public static final Comparator ID_COMPARATOR + = new Comparator() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mId, rhs.mId); + } + }; + + /** + * Compares the priority in ascending order. + */ + public static final Comparator PRIORITY_COMPARATOR + = new Comparator() { + @Override + public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { + return Long.compare(lhs.mPriority, rhs.mPriority); + } + }; + + /** + * Compares start time in ascending order and then priority in descending order and then ID in + * descending order. + */ + public static final Comparator START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(), + ID_COMPARATOR.reversed()); + + /** + * Builds scheduled recordings from programs. + */ + public static Builder builder(String inputId, Program p) { + return new Builder() + .setInputId(inputId) + .setChannelId(p.getChannelId()) + .setStartTimeMs(p.getStartTimeUtcMillis()).setEndTimeMs(p.getEndTimeUtcMillis()) + .setProgramId(p.getId()) + .setProgramTitle(p.getTitle()) + .setSeasonNumber(p.getSeasonNumber()) + .setEpisodeNumber(p.getEpisodeNumber()) + .setEpisodeTitle(p.getEpisodeTitle()) + .setProgramDescription(p.getDescription()) + .setProgramLongDescription(p.getLongDescription()) + .setProgramPosterArtUri(p.getPosterArtUri()) + .setProgramThumbnailUri(p.getThumbnailUri()) + .setType(TYPE_PROGRAM); + } + + public static Builder builder(String inputId, long channelId, long startTime, long endTime) { + return new Builder() + .setInputId(inputId) + .setChannelId(channelId) + .setStartTimeMs(startTime) + .setEndTimeMs(endTime) + .setType(TYPE_TIMED); + } + + /** + * Creates a new Builder with the values set from the {@link RecordedProgram}. + */ + @VisibleForTesting + public static Builder builder(RecordedProgram p) { + boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle()); + return new Builder() + .setInputId(p.getInputId()) + .setChannelId(p.getChannelId()) + .setType(isProgramRecording ? TYPE_PROGRAM : TYPE_TIMED) + .setStartTimeMs(p.getStartTimeUtcMillis()) + .setEndTimeMs(p.getEndTimeUtcMillis()) + .setProgramTitle(p.getTitle()) + .setSeasonNumber(p.getSeasonNumber()) + .setEpisodeNumber(p.getEpisodeNumber()) + .setEpisodeTitle(p.getEpisodeTitle()) + .setProgramDescription(p.getDescription()) + .setProgramLongDescription(p.getLongDescription()) + .setProgramPosterArtUri(p.getPosterArtUri()) + .setProgramThumbnailUri(p.getThumbnailUri()) + .setState(STATE_RECORDING_FINISHED); + } + + public static final class Builder { + private long mId = ID_NOT_SET; + private long mPriority = DvrScheduleManager.DEFAULT_PRIORITY; + private String mInputId; + private long mChannelId; + private long mProgramId = ID_NOT_SET; + private String mProgramTitle; + private @RecordingType int mType; + private long mStartTimeMs; + private long mEndTimeMs; + private String mSeasonNumber; + private String mEpisodeNumber; + private String mEpisodeTitle; + private String mProgramDescription; + private String mProgramLongDescription; + private String mProgramPosterArtUri; + private String mProgramThumbnailUri; + private @RecordingState int mState; + private long mSeriesRecordingId = ID_NOT_SET; + + private Builder() { } + + public Builder setId(long id) { + mId = id; + return this; + } + + public Builder setPriority(long priority) { + mPriority = priority; + return this; + } + + public Builder setInputId(String inputId) { + mInputId = inputId; + return this; + } + + public Builder setChannelId(long channelId) { + mChannelId = channelId; + return this; + } + + public Builder setProgramId(long programId) { + mProgramId = programId; + return this; + } + + public Builder setProgramTitle(String programTitle) { + mProgramTitle = programTitle; + return this; + } + + private Builder setType(@RecordingType int type) { + mType = type; + return this; + } + + public Builder setStartTimeMs(long startTimeMs) { + mStartTimeMs = startTimeMs; + return this; + } + + public Builder setEndTimeMs(long endTimeMs) { + mEndTimeMs = endTimeMs; + return this; + } + + public Builder setSeasonNumber(String seasonNumber) { + mSeasonNumber = seasonNumber; + return this; + } + + public Builder setEpisodeNumber(String episodeNumber) { + mEpisodeNumber = episodeNumber; + return this; + } + + public Builder setEpisodeTitle(String episodeTitle) { + mEpisodeTitle = episodeTitle; + return this; + } + + public Builder setProgramDescription(String description) { + mProgramDescription = description; + return this; + } + + public Builder setProgramLongDescription(String longDescription) { + mProgramLongDescription = longDescription; + return this; + } + + public Builder setProgramPosterArtUri(String programPosterArtUri) { + mProgramPosterArtUri = programPosterArtUri; + return this; + } + + public Builder setProgramThumbnailUri(String programThumbnailUri) { + mProgramThumbnailUri = programThumbnailUri; + return this; + } + + public Builder setState(@RecordingState int state) { + mState = state; + return this; + } + + public Builder setSeriesRecordingId(long seriesRecordingId) { + mSeriesRecordingId = seriesRecordingId; + return this; + } + + public ScheduledRecording build() { + return new ScheduledRecording(mId, mPriority, mInputId, mChannelId, mProgramId, + mProgramTitle, mType, mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, + mEpisodeTitle, mProgramDescription, mProgramLongDescription, + mProgramPosterArtUri, mProgramThumbnailUri, mState, mSeriesRecordingId); + } + } + + /** + * Creates {@link Builder} object from the given original {@code Recording}. + */ + public static Builder buildFrom(ScheduledRecording orig) { + return new Builder() + .setId(orig.mId) + .setInputId(orig.mInputId) + .setChannelId(orig.mChannelId) + .setEndTimeMs(orig.mEndTimeMs) + .setSeriesRecordingId(orig.mSeriesRecordingId) + .setPriority(orig.mPriority) + .setProgramId(orig.mProgramId) + .setProgramTitle(orig.mProgramTitle) + .setStartTimeMs(orig.mStartTimeMs) + .setSeasonNumber(orig.getSeasonNumber()) + .setEpisodeNumber(orig.getEpisodeNumber()) + .setEpisodeTitle(orig.getEpisodeTitle()) + .setProgramDescription(orig.getProgramDescription()) + .setProgramLongDescription(orig.getProgramLongDescription()) + .setProgramPosterArtUri(orig.getProgramPosterArtUri()) + .setProgramThumbnailUri(orig.getProgramThumbnailUri()) + .setState(orig.mState).setType(orig.mType); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS, STATE_RECORDING_FINISHED, + STATE_RECORDING_FAILED, STATE_RECORDING_CLIPPED, STATE_RECORDING_DELETED, + STATE_RECORDING_CANCELED}) + public @interface RecordingState {} + public static final int STATE_RECORDING_NOT_STARTED = 0; + public static final int STATE_RECORDING_IN_PROGRESS = 1; + public static final int STATE_RECORDING_FINISHED = 2; + public static final int STATE_RECORDING_FAILED = 3; + public static final int STATE_RECORDING_CLIPPED = 4; + public static final int STATE_RECORDING_DELETED = 5; + public static final int STATE_RECORDING_CANCELED = 6; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_TIMED, TYPE_PROGRAM}) + public @interface RecordingType {} + /** + * Record with given time range. + */ + public static final int TYPE_TIMED = 1; + /** + * Record with a given program. + */ + public static final int TYPE_PROGRAM = 2; + + @RecordingType private final int mType; + + /** + * Use this projection if you want to create {@link ScheduledRecording} object using + * {@link #fromCursor}. + */ + public static final String[] PROJECTION = { + // Columns must match what is read in #fromCursor + Schedules._ID, + Schedules.COLUMN_PRIORITY, + Schedules.COLUMN_TYPE, + Schedules.COLUMN_INPUT_ID, + Schedules.COLUMN_CHANNEL_ID, + Schedules.COLUMN_PROGRAM_ID, + Schedules.COLUMN_PROGRAM_TITLE, + Schedules.COLUMN_START_TIME_UTC_MILLIS, + Schedules.COLUMN_END_TIME_UTC_MILLIS, + Schedules.COLUMN_SEASON_NUMBER, + Schedules.COLUMN_EPISODE_NUMBER, + Schedules.COLUMN_EPISODE_TITLE, + Schedules.COLUMN_PROGRAM_DESCRIPTION, + Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, + Schedules.COLUMN_PROGRAM_POST_ART_URI, + Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, + Schedules.COLUMN_STATE, + Schedules.COLUMN_SERIES_RECORDING_ID}; + + /** + * Creates {@link ScheduledRecording} object from the given {@link Cursor}. + */ + public static ScheduledRecording fromCursor(Cursor c) { + int index = -1; + return new Builder() + .setId(c.getLong(++index)) + .setPriority(c.getLong(++index)) + .setType(recordingType(c.getString(++index))) + .setInputId(c.getString(++index)) + .setChannelId(c.getLong(++index)) + .setProgramId(c.getLong(++index)) + .setProgramTitle(c.getString(++index)) + .setStartTimeMs(c.getLong(++index)) + .setEndTimeMs(c.getLong(++index)) + .setSeasonNumber(c.getString(++index)) + .setEpisodeNumber(c.getString(++index)) + .setEpisodeTitle(c.getString(++index)) + .setProgramDescription(c.getString(++index)) + .setProgramLongDescription(c.getString(++index)) + .setProgramPosterArtUri(c.getString(++index)) + .setProgramThumbnailUri(c.getString(++index)) + .setState(recordingState(c.getString(++index))) + .setSeriesRecordingId(c.getLong(++index)) + .build(); + } + + public static ContentValues toContentValues(ScheduledRecording r) { + ContentValues values = new ContentValues(); + if (r.getId() != ID_NOT_SET) { + values.put(Schedules._ID, r.getId()); + } + values.put(Schedules.COLUMN_INPUT_ID, r.getInputId()); + values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId()); + values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId()); + values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle()); + values.put(Schedules.COLUMN_PRIORITY, r.getPriority()); + values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs()); + values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs()); + values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber()); + values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber()); + values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle()); + values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription()); + values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription()); + values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri()); + values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri()); + values.put(Schedules.COLUMN_STATE, recordingState(r.getState())); + values.put(Schedules.COLUMN_TYPE, recordingType(r.getType())); + if (r.getSeriesRecordingId() != ID_NOT_SET) { + values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId()); + } else { + values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID); + } + return values; + } + + public static ScheduledRecording fromParcel(Parcel in) { + return new Builder() + .setId(in.readLong()) + .setPriority(in.readLong()) + .setInputId(in.readString()) + .setChannelId(in.readLong()) + .setProgramId(in.readLong()) + .setProgramTitle(in.readString()) + .setType(in.readInt()) + .setStartTimeMs(in.readLong()) + .setEndTimeMs(in.readLong()) + .setSeasonNumber(in.readString()) + .setEpisodeNumber(in.readString()) + .setEpisodeTitle(in.readString()) + .setProgramDescription(in.readString()) + .setProgramLongDescription(in.readString()) + .setProgramPosterArtUri(in.readString()) + .setProgramThumbnailUri(in.readString()) + .setState(in.readInt()) + .setSeriesRecordingId(in.readLong()) + .build(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ScheduledRecording createFromParcel(Parcel in) { + return ScheduledRecording.fromParcel(in); + } + + @Override + public ScheduledRecording[] newArray(int size) { + return new ScheduledRecording[size]; + } + }; + + /** + * The ID internal to Live TV + */ + private long mId; + + /** + * The priority of this recording. + * + *

The highest number is recorded first. If there is a tie in priority then the higher id + * wins. + */ + private final long mPriority; + + private final String mInputId; + private final long mChannelId; + /** + * Optional id of the associated program. + */ + private final long mProgramId; + private final String mProgramTitle; + + private final long mStartTimeMs; + private final long mEndTimeMs; + private final String mSeasonNumber; + private final String mEpisodeNumber; + private final String mEpisodeTitle; + private final String mProgramDescription; + private final String mProgramLongDescription; + private final String mProgramPosterArtUri; + private final String mProgramThumbnailUri; + @RecordingState private final int mState; + private final long mSeriesRecordingId; + + private ScheduledRecording(long id, long priority, String inputId, long channelId, long programId, + String programTitle, @RecordingType int type, long startTime, long endTime, + String seasonNumber, String episodeNumber, String episodeTitle, + String programDescription, String programLongDescription, String programPosterArtUri, + String programThumbnailUri, @RecordingState int state, long seriesRecordingId) { + mId = id; + mPriority = priority; + mInputId = inputId; + mChannelId = channelId; + mProgramId = programId; + mProgramTitle = programTitle; + mType = type; + mStartTimeMs = startTime; + mEndTimeMs = endTime; + mSeasonNumber = seasonNumber; + mEpisodeNumber = episodeNumber; + mEpisodeTitle = episodeTitle; + mProgramDescription = programDescription; + mProgramLongDescription = programLongDescription; + mProgramPosterArtUri = programPosterArtUri; + mProgramThumbnailUri = programThumbnailUri; + mState = state; + mSeriesRecordingId = seriesRecordingId; + } + + /** + * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and + * {@link #TYPE_TIMED}. + */ + @RecordingType + public int getType() { + return mType; + } + + /** + * Returns schedules' input id. + */ + public String getInputId() { + return mInputId; + } + + /** + * Returns recorded {@link Channel}. + */ + public long getChannelId() { + return mChannelId; + } + + /** + * Return the optional program id + */ + public long getProgramId() { + return mProgramId; + } + + /** + * Return the optional program Title + */ + public String getProgramTitle() { + return mProgramTitle; + } + + /** + * Returns started time. + */ + public long getStartTimeMs() { + return mStartTimeMs; + } + + /** + * Returns ended time. + */ + public long getEndTimeMs() { + return mEndTimeMs; + } + + /** + * Returns the season number. + */ + public String getSeasonNumber() { + return mSeasonNumber; + } + + /** + * Returns the episode number. + */ + public String getEpisodeNumber() { + return mEpisodeNumber; + } + + /** + * Returns the episode title. + */ + public String getEpisodeTitle() { + return mEpisodeTitle; + } + + /** + * Returns the description of program. + */ + public String getProgramDescription() { + return mProgramDescription; + } + + /** + * Returns the long description of program. + */ + public String getProgramLongDescription() { + return mProgramLongDescription; + } + + /** + * Returns the poster uri of program. + */ + public String getProgramPosterArtUri() { + return mProgramPosterArtUri; + } + + /** + * Returns the thumb nail uri of program. + */ + public String getProgramThumbnailUri() { + return mProgramThumbnailUri; + } + + /** + * Returns duration. + */ + public long getDuration() { + return mEndTimeMs - mStartTimeMs; + } + + /** + * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED}, + * {@link #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, + * {@link #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and + * {@link #STATE_RECORDING_DELETED}. + */ + @RecordingState public int getState() { + return mState; + } + + /** + * Returns the ID of the {@link SeriesRecording} including this schedule. + */ + public long getSeriesRecordingId() { + return mSeriesRecordingId; + } + + public long getId() { + return mId; + } + + /** + * Sets the ID; + */ + public void setId(long id) { + mId = id; + } + + public long getPriority() { + return mPriority; + } + + /** + * Returns season number, episode number and episode title for display. + */ + public String getEpisodeDisplayTitle(Context context) { + if (!TextUtils.isEmpty(mEpisodeNumber)) { + String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; + if (TextUtils.equals(mSeasonNumber, "0")) { + // Do not show "S0: ". + return String.format(context.getResources().getString( + R.string.display_episode_title_format_no_season_number), + mEpisodeNumber, episodeTitle); + } else { + return String.format(context.getResources().getString( + R.string.display_episode_title_format), + mSeasonNumber, mEpisodeNumber, episodeTitle); + } + } + return mEpisodeTitle; + } + + /** + * Returns the program's title withe its season and episode number. + */ + public String getProgramTitleWithEpisodeNumber(Context context) { + if (TextUtils.isEmpty(mProgramTitle)) { + return mProgramTitle; + } + if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { + return TextUtils.isEmpty(mEpisodeNumber) ? mProgramTitle : context.getString( + R.string.program_title_with_episode_number_no_season, mProgramTitle, + mEpisodeNumber); + } else { + return context.getString(R.string.program_title_with_episode_number, mProgramTitle, + mSeasonNumber, mEpisodeNumber); + } + } + + /** + * 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}. + */ + private static @RecordingType int recordingType(String type) { + switch (type) { + case Schedules.TYPE_TIMED: + return TYPE_TIMED; + case Schedules.TYPE_PROGRAM: + return TYPE_PROGRAM; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + return TYPE_TIMED; + } + } + + /** + * Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}. + */ + private static String recordingType(@RecordingType int type) { + switch (type) { + case TYPE_TIMED: + return Schedules.TYPE_TIMED; + case TYPE_PROGRAM: + return Schedules.TYPE_PROGRAM; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type); + return Schedules.TYPE_TIMED; + } + } + + /** + * Converts a string to a @RecordingState int, defaulting to + * {@link #STATE_RECORDING_NOT_STARTED}. + */ + private static @RecordingState int recordingState(String state) { + switch (state) { + case Schedules.STATE_RECORDING_NOT_STARTED: + return STATE_RECORDING_NOT_STARTED; + case Schedules.STATE_RECORDING_IN_PROGRESS: + return STATE_RECORDING_IN_PROGRESS; + case Schedules.STATE_RECORDING_FINISHED: + return STATE_RECORDING_FINISHED; + case Schedules.STATE_RECORDING_FAILED: + return STATE_RECORDING_FAILED; + case Schedules.STATE_RECORDING_CLIPPED: + return STATE_RECORDING_CLIPPED; + case Schedules.STATE_RECORDING_DELETED: + return STATE_RECORDING_DELETED; + case Schedules.STATE_RECORDING_CANCELED: + return STATE_RECORDING_CANCELED; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + return STATE_RECORDING_NOT_STARTED; + } + } + + /** + * Converts a @RecordingState int to string, defaulting to + * {@link Schedules#STATE_RECORDING_NOT_STARTED}. + */ + private static String recordingState(@RecordingState int state) { + switch (state) { + case STATE_RECORDING_NOT_STARTED: + return Schedules.STATE_RECORDING_NOT_STARTED; + case STATE_RECORDING_IN_PROGRESS: + return Schedules.STATE_RECORDING_IN_PROGRESS; + case STATE_RECORDING_FINISHED: + return Schedules.STATE_RECORDING_FINISHED; + case STATE_RECORDING_FAILED: + return Schedules.STATE_RECORDING_FAILED; + case STATE_RECORDING_CLIPPED: + return Schedules.STATE_RECORDING_CLIPPED; + case STATE_RECORDING_DELETED: + return Schedules.STATE_RECORDING_DELETED; + case STATE_RECORDING_CANCELED: + return Schedules.STATE_RECORDING_CANCELED; + default: + SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state); + return Schedules.STATE_RECORDING_NOT_STARTED; + } + } + + /** + * Checks if the {@code period} overlaps with the recording time. + */ + public boolean isOverLapping(Range period) { + return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); + } + + /** + * Checks if the {@code schedule} overlaps with this schedule. + */ + public boolean isOverLapping(ScheduledRecording schedule) { + return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs(); + } + + @Override + public String toString() { + return "ScheduledRecording[" + mId + + "]" + + "(inputId=" + mInputId + + ",channelId=" + mChannelId + + ",programId=" + mProgramId + + ",programTitle=" + mProgramTitle + + ",type=" + mType + + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")" + + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")" + + ",seasonNumber=" + mSeasonNumber + + ",episodeNumber=" + mEpisodeNumber + + ",episodeTitle=" + mEpisodeTitle + + ",programDescription=" + mProgramDescription + + ",programLongDescription=" + mProgramLongDescription + + ",programPosterArtUri=" + mProgramPosterArtUri + + ",programThumbnailUri=" + mProgramThumbnailUri + + ",state=" + mState + + ",priority=" + mPriority + + ",seriesRecordingId=" + mSeriesRecordingId + + ")"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int paramInt) { + out.writeLong(mId); + out.writeLong(mPriority); + out.writeString(mInputId); + out.writeLong(mChannelId); + out.writeLong(mProgramId); + out.writeString(mProgramTitle); + out.writeInt(mType); + out.writeLong(mStartTimeMs); + out.writeLong(mEndTimeMs); + out.writeString(mSeasonNumber); + out.writeString(mEpisodeNumber); + out.writeString(mEpisodeTitle); + out.writeString(mProgramDescription); + out.writeString(mProgramLongDescription); + out.writeString(mProgramPosterArtUri); + out.writeString(mProgramThumbnailUri); + out.writeInt(mState); + out.writeLong(mSeriesRecordingId); + } + + /** + * Returns {@code true} if the recording is not started yet, otherwise @{code false}. + */ + public boolean isNotStarted() { + return mState == STATE_RECORDING_NOT_STARTED; + } + + /** + * Returns {@code true} if the recording is in progress, otherwise @{code false}. + */ + public boolean isInProgress() { + return mState == STATE_RECORDING_IN_PROGRESS; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ScheduledRecording)) { + return false; + } + ScheduledRecording r = (ScheduledRecording) obj; + return mId == r.mId + && mPriority == r.mPriority + && mChannelId == r.mChannelId + && mProgramId == r.mProgramId + && Objects.equals(mProgramTitle, r.mProgramTitle) + && mType == r.mType + && mStartTimeMs == r.mStartTimeMs + && mEndTimeMs == r.mEndTimeMs + && Objects.equals(mSeasonNumber, r.mSeasonNumber) + && Objects.equals(mEpisodeNumber, r.mEpisodeNumber) + && Objects.equals(mEpisodeTitle, r.mEpisodeTitle) + && Objects.equals(mProgramDescription, r.getProgramDescription()) + && Objects.equals(mProgramLongDescription, r.getProgramLongDescription()) + && Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri()) + && Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri()) + && mState == r.mState + && mSeriesRecordingId == r.mSeriesRecordingId; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mPriority, mChannelId, mProgramId, mProgramTitle, mType, + mStartTimeMs, mEndTimeMs, mSeasonNumber, mEpisodeNumber, mEpisodeTitle, + mProgramDescription, mProgramLongDescription, mProgramPosterArtUri, + mProgramThumbnailUri, mState, mSeriesRecordingId); + } + + /** + * Returns an array containing all of the elements in the list. + */ + public static ScheduledRecording[] toArray(Collection schedules) { + return schedules.toArray(new ScheduledRecording[schedules.size()]); + } +} 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/data/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java new file mode 100644 index 00000000..a0dec4a4 --- /dev/null +++ b/src/com/android/tv/dvr/data/SeriesInfo.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.data; + +/** + * Series information. + */ +public class SeriesInfo { + private final String mId; + private final String mTitle; + private final String mDescription; + private final String mLongDescription; + private final int[] mCanonicalGenreIds; + private final String mPosterUri; + private final String mPhotoUri; + + public SeriesInfo(String id, String title, String description, String longDescription, + int[] canonicalGenreIds, String posterUri, String photoUri) { + this.mId = id; + this.mTitle = title; + this.mDescription = description; + this.mLongDescription = longDescription; + this.mCanonicalGenreIds = canonicalGenreIds; + this.mPosterUri = posterUri; + this.mPhotoUri = photoUri; + } + + /** Returns the ID. **/ + public String getId() { + return mId; + } + + /** Returns the title. **/ + public String getTitle() { + return mTitle; + } + + /** Returns the description. **/ + public String getDescription() { + return mDescription; + } + + /** Returns the description. **/ + public String getLongDescription() { + return mLongDescription; + } + + /** Returns the canonical genre IDs. **/ + public int[] getCanonicalGenreIds() { + return mCanonicalGenreIds; + } + + /** Returns the poster URI. **/ + public String getPosterUri() { + return mPosterUri; + } + + /** Returns the photo URI. **/ + public String getPhotoUri() { + return mPhotoUri; + } +} diff --git a/src/com/android/tv/dvr/data/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java new file mode 100644 index 00000000..b7cf0f66 --- /dev/null +++ b/src/com/android/tv/dvr/data/SeriesRecording.java @@ -0,0 +1,756 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.data; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; +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; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +/** + * Schedules the recording of a Series of Programs. + * + *

+ * Contains the data needed to create new ScheduleRecordings as the programs become available in + * the EPG. + */ +public class SeriesRecording implements Parcelable { + /** + * Indicates that the ID is not assigned yet. + */ + public static final long ID_NOT_SET = 0; + + /** + * The default priority of this recording. + */ + public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL}) + public @interface ChannelOption {} + /** + * An option which indicates that the episodes in one channel are recorded. + */ + public static final int OPTION_CHANNEL_ONE = 0; + /** + * An option which indicates that the episodes in all the channels are recorded. + */ + public static final int OPTION_CHANNEL_ALL = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, + value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) + public @interface SeriesState {} + + /** + * The state indicates that the series recording is a normal one. + */ + public static final int STATE_SERIES_NORMAL = 0; + + /** + * The state indicates that the series recording is stopped. + */ + public static final int STATE_SERIES_STOPPED = 1; + + /** + * Compare priority in descending order. + */ + public static final Comparator PRIORITY_COMPARATOR = + new Comparator() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + int value = Long.compare(rhs.mPriority, lhs.mPriority); + if (value == 0) { + // New recording has the higher priority. + value = Long.compare(rhs.mId, lhs.mId); + } + return value; + } + }; + + /** + * Compare ID in ascending order. + */ + public static final Comparator ID_COMPARATOR = + new Comparator() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + return Long.compare(lhs.mId, rhs.mId); + } + }; + + /** + * Creates a new Builder with the values set from the series information of {@link BaseProgram}. + */ + public static Builder builder(String inputId, BaseProgram p) { + return new Builder() + .setInputId(inputId) + .setSeriesId(p.getSeriesId()) + .setChannelId(p.getChannelId()) + .setTitle(p.getTitle()) + .setDescription(p.getDescription()) + .setLongDescription(p.getLongDescription()) + .setCanonicalGenreIds(p.getCanonicalGenreIds()) + .setPosterUri(p.getPosterArtUri()) + .setPhotoUri(p.getThumbnailUri()); + } + + /** + * Creates a new Builder with the values set from an existing {@link SeriesRecording}. + */ + @VisibleForTesting + public static Builder buildFrom(SeriesRecording r) { + return new Builder() + .setId(r.mId) + .setInputId(r.getInputId()) + .setChannelId(r.getChannelId()) + .setPriority(r.getPriority()) + .setTitle(r.getTitle()) + .setDescription(r.getDescription()) + .setLongDescription(r.getLongDescription()) + .setSeriesId(r.getSeriesId()) + .setStartFromEpisode(r.getStartFromEpisode()) + .setStartFromSeason(r.getStartFromSeason()) + .setChannelOption(r.getChannelOption()) + .setCanonicalGenreIds(r.getCanonicalGenreIds()) + .setPosterUri(r.getPosterUri()) + .setPhotoUri(r.getPhotoUri()) + .setState(r.getState()); + } + + /** + * Use this projection if you want to create {@link SeriesRecording} object using + * {@link #fromCursor}. + */ + public static final String[] PROJECTION = { + // Columns must match what is read in fromCursor() + SeriesRecordings._ID, + SeriesRecordings.COLUMN_INPUT_ID, + SeriesRecordings.COLUMN_CHANNEL_ID, + SeriesRecordings.COLUMN_PRIORITY, + SeriesRecordings.COLUMN_TITLE, + SeriesRecordings.COLUMN_SHORT_DESCRIPTION, + SeriesRecordings.COLUMN_LONG_DESCRIPTION, + SeriesRecordings.COLUMN_SERIES_ID, + SeriesRecordings.COLUMN_START_FROM_EPISODE, + SeriesRecordings.COLUMN_START_FROM_SEASON, + SeriesRecordings.COLUMN_CHANNEL_OPTION, + SeriesRecordings.COLUMN_CANONICAL_GENRE, + SeriesRecordings.COLUMN_POSTER_URI, + SeriesRecordings.COLUMN_PHOTO_URI, + SeriesRecordings.COLUMN_STATE + }; + /** + * Creates {@link SeriesRecording} object from the given {@link Cursor}. + */ + public static SeriesRecording fromCursor(Cursor c) { + int index = -1; + return new Builder() + .setId(c.getLong(++index)) + .setInputId(c.getString(++index)) + .setChannelId(c.getLong(++index)) + .setPriority(c.getLong(++index)) + .setTitle(c.getString(++index)) + .setDescription(c.getString(++index)) + .setLongDescription(c.getString(++index)) + .setSeriesId(c.getString(++index)) + .setStartFromEpisode(c.getInt(++index)) + .setStartFromSeason(c.getInt(++index)) + .setChannelOption(channelOption(c.getString(++index))) + .setCanonicalGenreIds(c.getString(++index)) + .setPosterUri(c.getString(++index)) + .setPhotoUri(c.getString(++index)) + .setState(seriesRecordingState(c.getString(++index))) + .build(); + } + + /** + * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings} + * and the values from {@code r}. + */ + public static ContentValues toContentValues(SeriesRecording r) { + ContentValues values = new ContentValues(); + if (r.getId() != ID_NOT_SET) { + values.put(SeriesRecordings._ID, r.getId()); + } else { + values.putNull(SeriesRecordings._ID); + } + values.put(SeriesRecordings.COLUMN_INPUT_ID, r.getInputId()); + values.put(SeriesRecordings.COLUMN_CHANNEL_ID, r.getChannelId()); + values.put(SeriesRecordings.COLUMN_PRIORITY, r.getPriority()); + values.put(SeriesRecordings.COLUMN_TITLE, r.getTitle()); + values.put(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, r.getDescription()); + values.put(SeriesRecordings.COLUMN_LONG_DESCRIPTION, r.getLongDescription()); + values.put(SeriesRecordings.COLUMN_SERIES_ID, r.getSeriesId()); + values.put(SeriesRecordings.COLUMN_START_FROM_EPISODE, r.getStartFromEpisode()); + values.put(SeriesRecordings.COLUMN_START_FROM_SEASON, r.getStartFromSeason()); + values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION, + channelOption(r.getChannelOption())); + values.put(SeriesRecordings.COLUMN_CANONICAL_GENRE, + Utils.getCanonicalGenre(r.getCanonicalGenreIds())); + values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri()); + values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri()); + values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState())); + return values; + } + + private static String channelOption(@ChannelOption int option) { + switch (option) { + case OPTION_CHANNEL_ONE: + return SeriesRecordings.OPTION_CHANNEL_ONE; + case OPTION_CHANNEL_ALL: + return SeriesRecordings.OPTION_CHANNEL_ALL; + } + return SeriesRecordings.OPTION_CHANNEL_ONE; + } + + @ChannelOption private static int channelOption(String option) { + switch (option) { + case SeriesRecordings.OPTION_CHANNEL_ONE: + return OPTION_CHANNEL_ONE; + case SeriesRecordings.OPTION_CHANNEL_ALL: + return OPTION_CHANNEL_ALL; + } + return OPTION_CHANNEL_ONE; + } + + private static String seriesRecordingState(@SeriesState int state) { + switch (state) { + case STATE_SERIES_NORMAL: + return SeriesRecordings.STATE_SERIES_NORMAL; + case STATE_SERIES_STOPPED: + return SeriesRecordings.STATE_SERIES_STOPPED; + } + return SeriesRecordings.STATE_SERIES_NORMAL; + } + + @SeriesState private static int seriesRecordingState(String state) { + switch (state) { + case SeriesRecordings.STATE_SERIES_NORMAL: + return STATE_SERIES_NORMAL; + case SeriesRecordings.STATE_SERIES_STOPPED: + return STATE_SERIES_STOPPED; + } + return STATE_SERIES_NORMAL; + } + + /** + * Builder for {@link SeriesRecording}. + */ + public static class Builder { + private long mId = ID_NOT_SET; + private long mPriority = DvrScheduleManager.DEFAULT_SERIES_PRIORITY; + private String mTitle; + private String mDescription; + private String mLongDescription; + private String mInputId; + private long mChannelId; + private String mSeriesId; + private int mStartFromSeason = SeriesRecordings.THE_BEGINNING; + private int mStartFromEpisode = SeriesRecordings.THE_BEGINNING; + private int mChannelOption = OPTION_CHANNEL_ONE; + private int[] mCanonicalGenreIds; + private String mPosterUri; + private String mPhotoUri; + private int mState = SeriesRecording.STATE_SERIES_NORMAL; + + /** + * @see #getId() + */ + public Builder setId(long id) { + mId = id; + return this; + } + + /** + * @see #getPriority() () + */ + public Builder setPriority(long priority) { + mPriority = priority; + return this; + } + + /** + * @see #getTitle() + */ + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + /** + * @see #getDescription() + */ + public Builder setDescription(String description) { + mDescription = description; + return this; + } + + /** + * @see #getLongDescription() + */ + public Builder setLongDescription(String longDescription) { + mLongDescription = longDescription; + return this; + } + + /** + * @see #getInputId() + */ + public Builder setInputId(String inputId) { + mInputId = inputId; + return this; + } + + /** + * @see #getChannelId() + */ + public Builder setChannelId(long channelId) { + mChannelId = channelId; + return this; + } + + /** + * @see #getSeriesId() + */ + public Builder setSeriesId(String seriesId) { + mSeriesId = seriesId; + return this; + } + + /** + * @see #getStartFromSeason() + */ + public Builder setStartFromSeason(int startFromSeason) { + mStartFromSeason = startFromSeason; + return this; + } + + /** + * @see #getChannelOption() + */ + public Builder setChannelOption(@ChannelOption int option) { + mChannelOption = option; + return this; + } + + /** + * @see #getStartFromEpisode() + */ + public Builder setStartFromEpisode(int startFromEpisode) { + mStartFromEpisode = startFromEpisode; + return this; + } + + /** + * @see #getCanonicalGenreIds() + */ + public Builder setCanonicalGenreIds(String genres) { + mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres); + return this; + } + + /** + * @see #getCanonicalGenreIds() + */ + public Builder setCanonicalGenreIds(int[] canonicalGenreIds) { + mCanonicalGenreIds = canonicalGenreIds; + return this; + } + + /** + * @see #getPosterUri() + */ + public Builder setPosterUri(String posterUri) { + mPosterUri = posterUri; + return this; + } + + /** + * @see #getPhotoUri() + */ + public Builder setPhotoUri(String photoUri) { + mPhotoUri = photoUri; + return this; + } + + /** + * @see #getState() + */ + public Builder setState(@SeriesState int state) { + mState = state; + return this; + } + + /** + * Creates a new {@link SeriesRecording}. + */ + public SeriesRecording build() { + return new SeriesRecording(mId, mPriority, mTitle, mDescription, mLongDescription, + mInputId, mChannelId, mSeriesId, mStartFromSeason, mStartFromEpisode, + mChannelOption, mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); + } + } + + public static SeriesRecording fromParcel(Parcel in) { + return new Builder() + .setId(in.readLong()) + .setPriority(in.readLong()) + .setTitle(in.readString()) + .setDescription(in.readString()) + .setLongDescription(in.readString()) + .setInputId(in.readString()) + .setChannelId(in.readLong()) + .setSeriesId(in.readString()) + .setStartFromSeason(in.readInt()) + .setStartFromEpisode(in.readInt()) + .setChannelOption(in.readInt()) + .setCanonicalGenreIds(in.createIntArray()) + .setPosterUri(in.readString()) + .setPhotoUri(in.readString()) + .setState(in.readInt()) + .build(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SeriesRecording createFromParcel(Parcel in) { + return SeriesRecording.fromParcel(in); + } + + @Override + public SeriesRecording[] newArray(int size) { + return new SeriesRecording[size]; + } + }; + + private long mId; + private final long mPriority; + private final String mTitle; + private final String mDescription; + private final String mLongDescription; + private final String mInputId; + private final long mChannelId; + private final String mSeriesId; + private final int mStartFromSeason; + private final int mStartFromEpisode; + @ChannelOption private final int mChannelOption; + private final int[] mCanonicalGenreIds; + private final String mPosterUri; + private final String mPhotoUri; + @SeriesState private int mState; + + /** + * The input id of this SeriesRecording. + */ + public String getInputId() { + return mInputId; + } + + /** + * The channelId to match. The channel ID might not be valid when the channel option is "ALL". + */ + public long getChannelId() { + return mChannelId; + } + + /** + * The id of this SeriesRecording. + */ + public long getId() { + return mId; + } + + /** + * Sets the ID. + */ + public void setId(long id) { + mId = id; + } + + /** + * The priority of this recording. + * + *

The highest number is recorded first. If there is a tie in mPriority then the higher mId + * wins. + */ + public long getPriority() { + return mPriority; + } + + /** + * The series title. + */ + public String getTitle() { + return mTitle; + } + + /** + * The series description. + */ + public String getDescription() { + return mDescription; + } + + /** + * The long series description. + */ + public String getLongDescription() { + return mLongDescription; + } + + /** + * SeriesId when not null is used to match programs instead of using title and channelId. + * + *

SeriesId is an opaque but stable string. + */ + public String getSeriesId() { + return mSeriesId; + } + + /** + * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then + * only record episodes with a episodeNumber >= this + */ + public int getStartFromEpisode() { + return mStartFromEpisode; + } + + /** + * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a + * seasonNumber >= this + */ + public int getStartFromSeason() { + return mStartFromSeason; + } + + /** + * Returns the channel recording option. + */ + @ChannelOption public int getChannelOption() { + return mChannelOption; + } + + /** + * Returns the canonical genre ID's. + */ + public int[] getCanonicalGenreIds() { + return mCanonicalGenreIds; + } + + /** + * Returns the poster URI. + */ + public String getPosterUri() { + return mPosterUri; + } + + /** + * Returns the photo URI. + */ + public String getPhotoUri() { + return mPhotoUri; + } + + /** + * Returns the state of series recording. + */ + @SeriesState public int getState() { + return mState; + } + + /** + * Checks whether the series recording is stopped or not. + */ + public boolean isStopped() { + return mState == STATE_SERIES_STOPPED; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SeriesRecording)) return false; + SeriesRecording that = (SeriesRecording) o; + return mPriority == that.mPriority + && mChannelId == that.mChannelId + && mStartFromSeason == that.mStartFromSeason + && mStartFromEpisode == that.mStartFromEpisode + && Objects.equals(mId, that.mId) + && Objects.equals(mTitle, that.mTitle) + && Objects.equals(mDescription, that.mDescription) + && Objects.equals(mLongDescription, that.mLongDescription) + && Objects.equals(mSeriesId, that.mSeriesId) + && mChannelOption == that.mChannelOption + && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds) + && Objects.equals(mPosterUri, that.mPosterUri) + && Objects.equals(mPhotoUri, that.mPhotoUri) + && mState == that.mState; + } + + @Override + public int hashCode() { + return Objects.hash(mPriority, mChannelId, mStartFromSeason, mStartFromEpisode, mId, + mTitle, mDescription, mLongDescription, mSeriesId, mChannelOption, + mCanonicalGenreIds, mPosterUri, mPhotoUri, mState); + } + + @Override + public String toString() { + return "SeriesRecording{" + + "inputId=" + mInputId + + ", channelId=" + mChannelId + + ", id='" + mId + '\'' + + ", priority=" + mPriority + + ", title='" + mTitle + '\'' + + ", description='" + mDescription + '\'' + + ", longDescription='" + mLongDescription + '\'' + + ", startFromSeason=" + mStartFromSeason + + ", startFromEpisode=" + mStartFromEpisode + + ", channelOption=" + mChannelOption + + ", canonicalGenreIds=" + Arrays.toString(mCanonicalGenreIds) + + ", posterUri=" + mPosterUri + + ", photoUri=" + mPhotoUri + + ", state=" + mState + + '}'; + } + + private SeriesRecording(long id, long priority, String title, String description, + String longDescription, String inputId, long channelId, String seriesId, + int startFromSeason, int startFromEpisode, int channelOption, int[] canonicalGenreIds, + String posterUri, String photoUri, int state) { + this.mId = id; + this.mPriority = priority; + this.mTitle = title; + this.mDescription = description; + this.mLongDescription = longDescription; + this.mInputId = inputId; + this.mChannelId = channelId; + this.mSeriesId = seriesId; + this.mStartFromSeason = startFromSeason; + this.mStartFromEpisode = startFromEpisode; + this.mChannelOption = channelOption; + this.mCanonicalGenreIds = canonicalGenreIds; + this.mPosterUri = posterUri; + this.mPhotoUri = photoUri; + this.mState = state; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int paramInt) { + out.writeLong(mId); + out.writeLong(mPriority); + out.writeString(mTitle); + out.writeString(mDescription); + out.writeString(mLongDescription); + out.writeString(mInputId); + out.writeLong(mChannelId); + out.writeString(mSeriesId); + out.writeInt(mStartFromSeason); + out.writeInt(mStartFromEpisode); + out.writeInt(mChannelOption); + out.writeIntArray(mCanonicalGenreIds); + out.writeString(mPosterUri); + out.writeString(mPhotoUri); + out.writeInt(mState); + } + + /** + * Returns an array containing all of the elements in the list. + */ + public static SeriesRecording[] toArray(Collection series) { + return series.toArray(new SeriesRecording[series.size()]); + } + + /** + * Returns {@code true} if the {@code program} is part of the series and meets the season and + * episode constraints. + */ + public boolean matchProgram(Program program) { + return matchProgram(program, mChannelOption); + } + + /** + * Returns {@code true} if the {@code program} is part of the series and meets the season and + * episode constraints. It checks the channel option only if {@code checkChannelOption} is + * {@code true}. + */ + public boolean matchProgram(Program program, @ChannelOption int channelOption) { + String seriesId = program.getSeriesId(); + long channelId = program.getChannelId(); + String seasonNumber = program.getSeasonNumber(); + String episodeNumber = program.getEpisodeNumber(); + if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mChannelId != channelId)) { + return false; + } + // Season number and episode number matches if + // start_season_number < program_season_number + // || (start_season_number == program_season_number + // && start_episode_number <= program_episode_number). + if (mStartFromSeason == SeriesRecordings.THE_BEGINNING + || TextUtils.isEmpty(seasonNumber)) { + return true; + } else { + int intSeasonNumber; + try { + intSeasonNumber = Integer.valueOf(seasonNumber); + } catch (NumberFormatException e) { + return true; + } + if (intSeasonNumber > mStartFromSeason) { + return true; + } else if (intSeasonNumber < mStartFromSeason) { + return false; + } + } + if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING + || TextUtils.isEmpty(episodeNumber)) { + return true; + } else { + int intEpisodeNumber; + try { + intEpisodeNumber = Integer.valueOf(episodeNumber); + } catch (NumberFormatException e) { + return true; + } + return intEpisodeNumber >= mStartFromEpisode; + } + } +} diff --git a/src/com/android/tv/dvr/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/provider/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java new file mode 100644 index 00000000..8a0c2d19 --- /dev/null +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -0,0 +1,373 @@ +/* + * 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.provider; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.content.Context; +import android.database.ContentObserver; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; + +/** + * A class to synchronizes DVR DB with TvProvider. + * + *

The current implementation of AsyncDbTask allows only one task to run at a time, and all the + * other tasks are blocked until the current one finishes. As this class performs the low priority + * jobs which take long time, it should not block others if possible. For this reason, only one + * program is queried at a time and others are queued and will be executed on the other + * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask. + */ +@MainThread +@TargetApi(Build.VERSION_CODES.N) +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 mProgramIdQueue = new LinkedList<>(); + private QueryProgramTask mQueryProgramTask; + private final SeriesRecordingScheduler mSeriesRecordingScheduler; + private final ContentObserver mContentObserver = new ContentObserver(new Handler( + Looper.getMainLooper())) { + @SuppressLint("SwitchIntDef") + @Override + public void onChange(boolean selfChange, Uri uri) { + switch (TvProviderUriMatcher.match(uri)) { + case TvProviderUriMatcher.MATCH_PROGRAM: + if (DEBUG) Log.d(TAG, "onProgramsUpdated"); + onProgramsUpdated(); + break; + case TvProviderUriMatcher.MATCH_PROGRAM_ID: + if (DEBUG) { + Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); + } + onProgramUpdated(ContentUris.parseId(uri)); + break; + } + } + }; + + private final ChannelDataManager.Listener mChannelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + start(); + } + + @Override + public void onChannelListUpdated() { + onChannelsUpdated(); + } + + @Override + public void onChannelBrowsableChanged() { } + }; + + private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + addProgramIdToCheckIfNeeded(schedule); + } + startNextUpdateIfNeeded(); + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + addProgramIdToCheckIfNeeded(schedule); + } + startNextUpdateIfNeeded(); + } + }; + + public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(), + TvApplication.getSingletons(context).getDvrManager(), + SeriesRecordingScheduler.getInstance(context)); + } + + @VisibleForTesting + DvrDbSync(Context context, DvrDataManagerImpl dataManager, + ChannelDataManager channelDataManager, DvrManager dvrManager, + SeriesRecordingScheduler seriesRecordingScheduler) { + mContext = context; + mDvrManager = dvrManager; + mDataManager = dataManager; + mChannelDataManager = channelDataManager; + mSeriesRecordingScheduler = seriesRecordingScheduler; + } + + /** + * Starts the DB sync. + */ + public void start() { + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener(mChannelDataManagerListener); + return; + } + mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, + mContentObserver); + mDataManager.addScheduledRecordingListener(mScheduleListener); + onChannelsUpdated(); + onProgramsUpdated(); + } + + /** + * Stops the DB sync. + */ + public void stop() { + mProgramIdQueue.clear(); + if (mQueryProgramTask != null) { + mQueryProgramTask.cancel(true); + } + mChannelDataManager.removeListener(mChannelDataManagerListener); + mDataManager.removeScheduledRecordingListener(mScheduleListener); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + private void onChannelsUpdated() { + List seriesRecordingsToUpdate = new ArrayList<>(); + for (SeriesRecording r : mDataManager.getSeriesRecordings()) { + if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r) + .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) + .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + } + } + if (!seriesRecordingsToUpdate.isEmpty()) { + mDataManager.updateSeriesRecording( + SeriesRecording.toArray(seriesRecordingsToUpdate)); + } + List schedulesToRemove = new ArrayList<>(); + for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { + if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + schedulesToRemove.add(r); + mProgramIdQueue.remove(r.getProgramId()); + } + } + if (!schedulesToRemove.isEmpty()) { + mDataManager.removeScheduledRecording( + ScheduledRecording.toArray(schedulesToRemove)); + } + } + + private void onProgramsUpdated() { + for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) { + addProgramIdToCheckIfNeeded(schedule); + } + startNextUpdateIfNeeded(); + } + + private void onProgramUpdated(long programId) { + addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId)); + startNextUpdateIfNeeded(); + } + + private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) { + if (schedule == null) { + return; + } + long programId = schedule.getProgramId(); + if (programId != ScheduledRecording.ID_NOT_SET + && !mProgramIdQueue.contains(programId) + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); + mProgramIdQueue.offer(programId); + // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the + // schedule updates finish. + // Note that the SeriesRecordingScheduler should be paused even though the program to + // check is not episodic because it can be changed to the episodic program after the + // update, which affect the SeriesRecordingScheduler. + mSeriesRecordingScheduler.pauseUpdate(); + } + } + + private void startNextUpdateIfNeeded() { + if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) { + return; + } + if (!mProgramIdQueue.isEmpty()) { + if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek()); + mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll()); + mQueryProgramTask.executeOnDbThread(); + } else { + mSeriesRecordingScheduler.resumeUpdate(); + } + } + + @VisibleForTesting + void handleUpdateProgram(Program program, long programId) { + Set seriesRecordingsToUpdate = new HashSet<>(); + ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); + if (schedule != null + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + if (program == null) { + mDataManager.removeScheduledRecording(schedule); + if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (seriesRecording != null) { + seriesRecordingsToUpdate.add(seriesRecording); + } + } + } else { + long currentTimeMs = System.currentTimeMillis(); + ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) + .setEndTimeMs(program.getEndTimeUtcMillis()) + .setSeasonNumber(program.getSeasonNumber()) + .setEpisodeNumber(program.getEpisodeNumber()) + .setEpisodeTitle(program.getEpisodeTitle()) + .setProgramDescription(program.getDescription()) + .setProgramLongDescription(program.getLongDescription()) + .setProgramPosterArtUri(program.getPosterArtUri()) + .setProgramThumbnailUri(program.getThumbnailUri()); + boolean needUpdate = false; + // Check the series recording. + SeriesRecording seriesRecordingForOldSchedule = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (program.getSeriesId() != null) { + // New program belongs to a series. + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(program.getSeriesId()); + if (seriesRecording == null) { + // The new program is episodic while the previous one isn't. + SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording( + program, Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); + builder.setSeriesRecordingId(newSeriesRecording.getId()); + needUpdate = true; + } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { + // The new program belongs to the other series. + builder.setSeriesRecordingId(seriesRecording.getId()); + needUpdate = true; + seriesRecordingsToUpdate.add(seriesRecording); + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } else if (!Objects.equals(schedule.getSeasonNumber(), + program.getSeasonNumber()) + || !Objects.equals(schedule.getEpisodeNumber(), + program.getEpisodeNumber())) { + // The episode number has been changed. + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } + } else if (seriesRecordingForOldSchedule != null) { + // Old program belongs to a series but the new one doesn't. + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + // Change start time only when the recording is not started yet. + boolean needToChangeStartTime = + schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS + && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); + if (needToChangeStartTime) { + builder.setStartTimeMs(program.getStartTimeUtcMillis()); + needUpdate = true; + } + if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() + || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) + || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) + || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) + || !Objects.equals(schedule.getProgramDescription(), + program.getDescription()) + || !Objects.equals(schedule.getProgramLongDescription(), + program.getLongDescription()) + || !Objects.equals(schedule.getProgramPosterArtUri(), + program.getPosterArtUri()) + || !Objects.equals(schedule.getProgramThumbnailUri(), + program.getThumbnailUri())) { + mDataManager.updateScheduledRecording(builder.build()); + } + if (!seriesRecordingsToUpdate.isEmpty()) { + // The series recordings will be updated after it's resumed. + mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); + } + } + } + } + + private class QueryProgramTask extends AsyncQueryProgramTask { + private final long mProgramId; + + QueryProgramTask(long programId) { + super(mContext.getContentResolver(), programId); + mProgramId = programId; + } + + @Override + protected void onCancelled(Program program) { + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } + startNextUpdateIfNeeded(); + } + + @Override + protected void onPostExecute(Program program) { + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } + handleUpdateProgram(program, mProgramId); + startNextUpdateIfNeeded(); + } + } +} diff --git a/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java new file mode 100644 index 00000000..ba0aca51 --- /dev/null +++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java @@ -0,0 +1,329 @@ +/* + * 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.provider; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; + +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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. + */ +@TargetApi(Build.VERSION_CODES.N) +abstract public class EpisodicProgramLoadTask { + private static final String TAG = "EpisodicProgramLoadTask"; + + private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); + private static final int START_TIME_INDEX = + Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS); + private static final int RECORDING_PROHIBITED_INDEX = + Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); + + private static final String PARAM_START_TIME = "start_time"; + private static final String PARAM_END_TIME = "end_time"; + + private static final String PROGRAM_PREDICATE = + Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = + Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; + private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; + + private final Context mContext; + private final DvrDataManager mDataManager; + private boolean mQueryAllChannels; + private boolean mLoadCurrentProgram; + private boolean mLoadScheduledEpisode; + private boolean mLoadDisallowedProgram; + // If true, match programs with OPTION_CHANNEL_ALL. + private boolean mIgnoreChannelOption; + private final ArrayList mSeriesRecordings = new ArrayList<>(); + private AsyncProgramQueryTask mProgramQueryTask; + + /** + * + * Constructor used to load programs for one series recording with the given channel option. + */ + public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { + this(context, Collections.singletonList(seriesRecording)); + } + + /** + * Constructor used to load programs for multiple series recordings. The channel option is + * {@link SeriesRecording#OPTION_CHANNEL_ALL}. + */ + public EpisodicProgramLoadTask(Context context, Collection seriesRecordings) { + mContext = context.getApplicationContext(); + mDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mSeriesRecordings.addAll(seriesRecordings); + } + + /** + * Returns the series recordings. + */ + public List getSeriesRecordings() { + return mSeriesRecordings; + } + + /** + * Returns the program query task. It is {@code null} until it is executed. + */ + @Nullable + public AsyncProgramQueryTask getTask() { + return mProgramQueryTask; + } + + /** + * Enables loading current programs. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadCurrentProgram = loadCurrentProgram; + return this; + } + + /** + * Enables already schedules episodes. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadScheduledEpisode = loadScheduledEpisode; + return this; + } + + /** + * Enables loading disallowed programs whose schedules were removed manually by the user. + * The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadDisallowedProgram = loadDisallowedProgram; + return this; + } + + /** + * Gives the option whether to ignore the channel option when matching programs. + * If {@code ignoreChannelOption} is {@code true}, the program will be matched with + * {@link SeriesRecording#OPTION_CHANNEL_ALL} option. + */ + public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mIgnoreChannelOption = ignoreChannelOption; + return this; + } + + /** + * Executes the task. + * + * @see com.android.tv.util.AsyncDbTask#executeOnDbThread + */ + public void execute() { + if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't execute task: the task is already running.")) { + mQueryAllChannels = mSeriesRecordings.size() > 1 + || mSeriesRecordings.get(0).getChannelOption() + == SeriesRecording.OPTION_CHANNEL_ALL + || mIgnoreChannelOption; + mProgramQueryTask = createTask(); + mProgramQueryTask.executeOnDbThread(); + } + } + + /** + * Cancels the task. + * + * @see android.os.AsyncTask#cancel + */ + public void cancel(boolean mayInterruptIfRunning) { + if (mProgramQueryTask != null) { + mProgramQueryTask.cancel(mayInterruptIfRunning); + } + } + + /** + * Runs on the UI thread after the program loading finishes successfully. + */ + protected void onPostExecute(List programs) { + } + + /** + * Runs on the UI thread after the program loading was canceled. + */ + protected void onCancelled(List programs) { + } + + private AsyncProgramQueryTask createTask() { + SqlParams sqlParams = createSqlParams(); + return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, + sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) { + @Override + protected void onPostExecute(List programs) { + EpisodicProgramLoadTask.this.onPostExecute(programs); + } + + @Override + protected void onCancelled(List programs) { + EpisodicProgramLoadTask.this.onCancelled(programs); + } + }; + } + + private SqlParams createSqlParams() { + SqlParams sqlParams = new SqlParams(); + if (PermissionUtils.hasAccessAllEpg(mContext)) { + sqlParams.uri = Programs.CONTENT_URI; + // Base + StringBuilder selection = new StringBuilder(mLoadCurrentProgram + ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE); + List args = new ArrayList<>(); + args.add(Long.toString(System.currentTimeMillis())); + // Channel option + if (!mQueryAllChannels) { + selection.append(" AND ").append(CHANNEL_ID_PREDICATE); + args.add(Long.toString(mSeriesRecordings.get(0).getChannelId())); + } + // Title + if (mSeriesRecordings.size() == 1) { + selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE); + args.add(mSeriesRecordings.get(0).getTitle()); + } + sqlParams.selection = selection.toString(); + sqlParams.selectionArgs = args.toArray(new String[args.size()]); + sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings); + } else { + // The query includes the current program. Will be filtered if needed. + if (mQueryAllChannels) { + sqlParams.uri = Programs.CONTENT_URI.buildUpon() + .appendQueryParameter(PARAM_START_TIME, + String.valueOf(System.currentTimeMillis())) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) + .build(); + } else { + sqlParams.uri = TvContract.buildProgramsUriForChannel( + mSeriesRecordings.get(0).getChannelId(), + System.currentTimeMillis(), Long.MAX_VALUE); + } + sqlParams.selection = null; + sqlParams.selectionArgs = null; + sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings); + } + return sqlParams; + } + + /** + * 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 mDisallowedProgramIds = new HashSet<>(); + private final Set mSeasonEpisodeNumbers = new HashSet<>(); + + SeriesRecordingCursorFilter(List seriesRecordings) { + if (!mLoadDisallowedProgram) { + mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); + } + if (!mLoadScheduledEpisode) { + Set seriesRecordingIds = new HashSet<>(); + for (SeriesRecording r : seriesRecordings) { + seriesRecordingIds.add(r.getId()); + } + for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { + if (seriesRecordingIds.contains(r.getSeriesRecordingId()) + && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r)); + } + } + } + } + + @Override + @WorkerThread + public boolean filter(Cursor c) { + if (!mLoadDisallowedProgram + && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { + return false; + } + Program program = Program.fromCursor(c); + for (SeriesRecording seriesRecording : mSeriesRecordings) { + boolean programMatches; + if (mIgnoreChannelOption) { + programMatches = seriesRecording.matchProgram(program, + SeriesRecording.OPTION_CHANNEL_ALL); + } else { + programMatches = seriesRecording.matchProgram(program); + } + if (programMatches) { + return mLoadScheduledEpisode + || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); + } + } + return false; + } + } + + private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { + SeriesRecordingCursorFilterForNonSystem(List seriesRecordings) { + super(seriesRecordings); + } + + @Override + public boolean filter(Cursor c) { + return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) + && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); + } + } + + private static class SqlParams { + public Uri uri; + public String selection; + public String[] selectionArgs; + public CursorFilter filter; + } +} diff --git a/src/com/android/tv/dvr/recorder/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java new file mode 100644 index 00000000..8aa90116 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.recorder; + +import android.annotation.TargetApi; +import android.content.ContentUris; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.Build; +import android.os.Message; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.ArraySet; +import android.util.Log; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.OnTvViewChannelChangeListener; +import com.android.tv.MainActivity; +import com.android.tv.TvApplication; +import com.android.tv.common.WeakHandler; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import 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; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Checking the runtime conflict of DVR recording. + *

+ * This class runs only while the {@link MainActivity} is resumed and holds the upcoming conflicts. + */ +@TargetApi(Build.VERSION_CODES.N) +@MainThread +public class ConflictChecker { + private static final String TAG = "ConflictChecker"; + private static final boolean DEBUG = false; + + private static final int MSG_CHECK_CONFLICT = 1; + + private static final long CHECK_RETRY_PERIOD_MS = TimeUnit.SECONDS.toMillis(30); + + /** + * To show watch conflict dialog, the start time of the earliest conflicting schedule should be + * less than or equal to this time. + */ + private static final long MAX_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.MINUTES.toMillis(5); + /** + * To show watch conflict dialog, the start time of the earliest conflicting schedule should be + * greater than or equal to this time. + */ + private static final long MIN_WATCH_CONFLICT_CHECK_TIME_MS = TimeUnit.SECONDS.toMillis(30); + + private final MainActivity mMainActivity; + private final ChannelDataManager mChannelDataManager; + private final DvrScheduleManager mScheduleManager; + private final InputSessionManager mSessionManager; + private final ConflictCheckerHandler mHandler = new ConflictCheckerHandler(this); + + private final List mUpcomingConflicts = new ArrayList<>(); + private final Set mOnUpcomingConflictChangeListeners = + new ArraySet<>(); + private final Map> mCheckedConflictsMap = new HashMap<>(); + + private final ScheduledRecordingListener mScheduledRecordingListener = + new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + scheduledRecordings); + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + scheduledRecordings); + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingStatusChanged: " + scheduledRecordings); + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } + }; + + private final OnTvViewChannelChangeListener mOnTvViewChannelChangeListener = + new OnTvViewChannelChangeListener() { + @Override + public void onTvViewChannelChange(@Nullable Uri channelUri) { + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + } + }; + + private boolean mStarted; + + public ConflictChecker(MainActivity mainActivity) { + mMainActivity = mainActivity; + ApplicationSingletons appSingletons = TvApplication.getSingletons(mainActivity); + mChannelDataManager = appSingletons.getChannelDataManager(); + mScheduleManager = appSingletons.getDvrScheduleManager(); + mSessionManager = appSingletons.getInputSessionManager(); + } + + /** + * Starts checking the conflict. + */ + public void start() { + if (mStarted) { + return; + } + mStarted = true; + mHandler.sendEmptyMessage(MSG_CHECK_CONFLICT); + mScheduleManager.addScheduledRecordingListener(mScheduledRecordingListener); + mSessionManager.addOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); + } + + /** + * Stops checking the conflict. + */ + public void stop() { + if (!mStarted) { + return; + } + mStarted = false; + mSessionManager.removeOnTvViewChannelChangeListener(mOnTvViewChannelChangeListener); + mScheduleManager.removeScheduledRecordingListener(mScheduledRecordingListener); + mHandler.removeCallbacksAndMessages(null); + } + + /** + * Returns the upcoming conflicts. + */ + public List getUpcomingConflicts() { + return new ArrayList<>(mUpcomingConflicts); + } + + /** + * Adds a {@link OnUpcomingConflictChangeListener}. + */ + public void addOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { + mOnUpcomingConflictChangeListeners.add(listener); + } + + /** + * Removes the {@link OnUpcomingConflictChangeListener}. + */ + public void removeOnUpcomingConflictChangeListener(OnUpcomingConflictChangeListener listener) { + mOnUpcomingConflictChangeListeners.remove(listener); + } + + private void notifyUpcomingConflictChanged() { + for (OnUpcomingConflictChangeListener l : mOnUpcomingConflictChangeListeners) { + l.onUpcomingConflictChange(); + } + } + + /** + * Remembers the user's decision to record while watching the channel. + */ + public void setCheckedConflictsForChannel(long mChannelId, List conflicts) { + mCheckedConflictsMap.put(mChannelId, new ArrayList<>(conflicts)); + } + + void onCheckConflict() { + // Checks the conflicting schedules and setup the next re-check time. + // If there are upcoming conflicts soon, it opens the conflict dialog. + if (DEBUG) Log.d(TAG, "Handling MSG_CHECK_CONFLICT"); + mHandler.removeMessages(MSG_CHECK_CONFLICT); + mUpcomingConflicts.clear(); + if (!mScheduleManager.isInitialized() + || !mChannelDataManager.isDbLoadFinished()) { + mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, CHECK_RETRY_PERIOD_MS); + notifyUpcomingConflictChanged(); + return; + } + if (mSessionManager.getCurrentTvViewChannelUri() == null) { + // As MainActivity is not using a tuner, no need to check the conflict. + notifyUpcomingConflictChanged(); + return; + } + Uri channelUri = mSessionManager.getCurrentTvViewChannelUri(); + if (TvContract.isChannelUriForPassthroughInput(channelUri)) { + notifyUpcomingConflictChanged(); + return; + } + long channelId = ContentUris.parseId(channelUri); + Channel channel = mChannelDataManager.getChannel(channelId); + // The conflicts caused by watching the channel. + List conflicts = mScheduleManager + .getConflictingSchedulesForWatching(channel.getId()); + long earliestToCheck = Long.MAX_VALUE; + long currentTimeMs = System.currentTimeMillis(); + for (ScheduledRecording schedule : conflicts) { + long startTimeMs = schedule.getStartTimeMs(); + if (startTimeMs < currentTimeMs + MIN_WATCH_CONFLICT_CHECK_TIME_MS) { + // The start time of the upcoming conflict remains less than the minimum + // check time. + continue; + } + if (startTimeMs > currentTimeMs + MAX_WATCH_CONFLICT_CHECK_TIME_MS) { + // The start time of the upcoming conflict remains greater than the + // maximum check time. Setup the next re-check time. + long nextCheckTimeMs = startTimeMs - MAX_WATCH_CONFLICT_CHECK_TIME_MS; + if (earliestToCheck > nextCheckTimeMs) { + earliestToCheck = nextCheckTimeMs; + } + } else { + // Found upcoming conflicts which will start soon. + mUpcomingConflicts.add(schedule); + // The schedule will be removed from the "upcoming conflict" when the + // recording is almost started. + long nextCheckTimeMs = startTimeMs - MIN_WATCH_CONFLICT_CHECK_TIME_MS; + if (earliestToCheck > nextCheckTimeMs) { + earliestToCheck = nextCheckTimeMs; + } + } + } + if (earliestToCheck != Long.MAX_VALUE) { + mHandler.sendEmptyMessageDelayed(MSG_CHECK_CONFLICT, + earliestToCheck - currentTimeMs); + } + if (DEBUG) Log.d(TAG, "upcoming conflicts: " + mUpcomingConflicts); + notifyUpcomingConflictChanged(); + if (!mUpcomingConflicts.isEmpty() + && !DvrUiHelper.isChannelWatchConflictDialogShown(mMainActivity)) { + // Don't show the conflict dialog if the user already knows. + List checkedConflicts = mCheckedConflictsMap.get( + channel.getId()); + if (checkedConflicts == null + || !checkedConflicts.containsAll(mUpcomingConflicts)) { + DvrUiHelper.showChannelWatchConflictDialog(mMainActivity, channel); + } + } + } + + private static class ConflictCheckerHandler extends WeakHandler { + ConflictCheckerHandler(ConflictChecker conflictChecker) { + super(conflictChecker); + } + + @Override + protected void handleMessage(Message msg, @NonNull ConflictChecker conflictChecker) { + switch (msg.what) { + case MSG_CHECK_CONFLICT: + conflictChecker.onCheckConflict(); + break; + } + } + } + + /** + * A listener for the change of upcoming conflicts. + */ + public interface OnUpcomingConflictChangeListener { + void onUpcomingConflictChange(); + } +} diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java new file mode 100644 index 00000000..08ffaf86 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.HandlerThread; +import android.os.IBinder; +import android.support.annotation.Nullable; +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; + +/** + * DVR Scheduler service. + * + *

This service is responsible for: + *

+ * + *

The service does not stop it self. + */ +public class DvrRecordingService extends Service { + private static final String TAG = "DvrRecordingService"; + 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); + } + + private final Clock mClock = Clock.SYSTEM; + private RecurringRunner mReaperRunner; + + 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() { + TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(); + SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); + ApplicationSingletons singletons = TvApplication.getSingletons(this); + 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) { + mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); + mHandlerThread.start(); + mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(), + singletons.getInputSessionManager(), dataManager, + singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), this, + mClock, alarmManager); + mScheduler.start(); + } + mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), + new ScheduledProgramReaper(dataManager, mClock), null); + mReaperRunner.start(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); + mScheduler.update(); + return START_STICKY; + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mReaperRunner.stop(); + mScheduler.stop(); + mScheduler = null; + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread = null; + } + mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener); + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @VisibleForTesting + void setScheduler(Scheduler scheduler) { + Log.i(TAG, "Setting scheduler for tests to " + scheduler); + mScheduler = scheduler; + } +} diff --git a/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java new file mode 100644 index 00000000..8c6ee145 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import com.android.tv.TvApplication; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Signals the DVR to start recording shows soon. + */ +public class DvrStartRecordingReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + TvApplication.setCurrentRunningProcess(context, true); + DvrRecordingService.startService(context); + } +} diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java new file mode 100644 index 00000000..46546a76 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java @@ -0,0 +1,435 @@ +/* + * 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.recorder; + +import android.content.Context; +import android.media.tv.TvInputInfo; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.util.Log; +import android.util.LongSparseArray; + +import com.android.tv.InputSessionManager; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * The scheduler for a TV input. + */ +public class InputTaskScheduler { + private static final String TAG = "InputTaskScheduler"; + private static final boolean DEBUG = false; + + private static final int MSG_ADD_SCHEDULED_RECORDING = 1; + private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2; + private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3; + private static final int MSG_BUILD_SCHEDULE = 4; + private static final int MSG_STOP_SCHEDULE = 5; + + private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f; + + // The candidate comparator should be the consistent with + // DvrScheduleManager#CANDIDATE_COMPARATOR. + private static final Comparator CANDIDATE_COMPARATOR = + new CompositeComparator<>( + RecordingTask.PRIORITY_COMPARATOR, + RecordingTask.END_TIME_COMPARATOR, + RecordingTask.ID_COMPARATOR); + + /** + * Returns the comparator which the schedules are sorted with when executed. + */ + public static Comparator getRecordingOrderComparator() { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; + } + + /** + * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. + */ + public final class HandlerWrapper extends Handler { + public static final int MESSAGE_REMOVE = 999; + private final long mId; + private final RecordingTask mTask; + + HandlerWrapper(Looper looper, ScheduledRecording scheduledRecording, + RecordingTask recordingTask) { + super(looper, recordingTask); + mId = scheduledRecording.getId(); + mTask = recordingTask; + mTask.setHandler(this); + } + + @Override + public void handleMessage(Message msg) { + // The RecordingTask gets a chance first. + // It must return false to pass this message to here. + if (msg.what == MESSAGE_REMOVE) { + if (DEBUG) Log.d(TAG, "done " + mId); + mPendingRecordings.remove(mId); + } + removeCallbacksAndMessages(null); + mHandler.removeMessages(MSG_BUILD_SCHEDULE); + mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); + super.handleMessage(msg); + } + } + + private TvInputInfo mInput; + private final Looper mLooper; + private final ChannelDataManager mChannelDataManager; + private final DvrManager mDvrManager; + private final WritableDvrDataManager mDataManager; + private final InputSessionManager mSessionManager; + private final Clock mClock; + private final Context mContext; + + private final LongSparseArray mPendingRecordings = new LongSparseArray<>(); + private final Map mWaitingSchedules = new ArrayMap<>(); + private final Handler mMainThreadHandler; + private final Handler mHandler; + private final Object mInputLock = new Object(); + private final RecordingTaskFactory mRecordingTaskFactory; + + public InputTaskScheduler(Context context, TvInputInfo input, Looper looper, + ChannelDataManager channelDataManager, DvrManager dvrManager, + DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) { + this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager, + clock, new Handler(Looper.getMainLooper()), null, null); + } + + @VisibleForTesting + InputTaskScheduler(Context context, TvInputInfo input, Looper looper, + ChannelDataManager channelDataManager, DvrManager dvrManager, + DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, + Handler mainThreadHandler, @Nullable Handler workerThreadHandler, + RecordingTaskFactory recordingTaskFactory) { + if (DEBUG) Log.d(TAG, "Creating scheduler for " + input); + mContext = context; + mInput = input; + mLooper = looper; + mChannelDataManager = channelDataManager; + mDvrManager = dvrManager; + mDataManager = (WritableDvrDataManager) dataManager; + mSessionManager = sessionManager; + mClock = clock; + mMainThreadHandler = mainThreadHandler; + mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory + : new RecordingTaskFactory() { + @Override + public RecordingTask createRecordingTask(ScheduledRecording schedule, Channel channel, + DvrManager dvrManager, InputSessionManager sessionManager, + WritableDvrDataManager dataManager, Clock clock) { + return new RecordingTask(mContext, schedule, channel, mDvrManager, mSessionManager, + mDataManager, mClock); + } + }; + if (workerThreadHandler == null) { + mHandler = new WorkerThreadHandler(looper); + } else { + mHandler = workerThreadHandler; + } + } + + /** + * Adds a {@link ScheduledRecording}. + */ + public void addSchedule(ScheduledRecording schedule) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_ADD_SCHEDULED_RECORDING, schedule)); + } + + @VisibleForTesting + void handleAddSchedule(ScheduledRecording schedule) { + if (mPendingRecordings.get(schedule.getId()) != null + || mWaitingSchedules.containsKey(schedule.getId())) { + return; + } + mWaitingSchedules.put(schedule.getId(), schedule); + mHandler.removeMessages(MSG_BUILD_SCHEDULE); + mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); + } + + /** + * Removes the {@link ScheduledRecording}. + */ + public void removeSchedule(ScheduledRecording schedule) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_REMOVE_SCHEDULED_RECORDING, schedule)); + } + + @VisibleForTesting + void handleRemoveSchedule(ScheduledRecording schedule) { + HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); + if (wrapper != null) { + wrapper.mTask.cancel(); + return; + } + if (mWaitingSchedules.containsKey(schedule.getId())) { + mWaitingSchedules.remove(schedule.getId()); + mHandler.removeMessages(MSG_BUILD_SCHEDULE); + mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); + } + } + + /** + * Updates the {@link ScheduledRecording}. + */ + public void updateSchedule(ScheduledRecording schedule) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_SCHEDULED_RECORDING, schedule)); + } + + @VisibleForTesting + void handleUpdateSchedule(ScheduledRecording schedule) { + HandlerWrapper wrapper = mPendingRecordings.get(schedule.getId()); + if (wrapper != null) { + if (schedule.getStartTimeMs() > mClock.currentTimeMillis() + && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) { + // It shouldn't have started. Cancel and put to the waiting list. + // The schedules will be rebuilt when the task is removed. + // The reschedule is called in Scheduler. + wrapper.mTask.cancel(); + mWaitingSchedules.put(schedule.getId(), schedule); + return; + } + wrapper.sendMessage(wrapper.obtainMessage(RecordingTask.MSG_UDPATE_SCHEDULE, schedule)); + return; + } + if (mWaitingSchedules.containsKey(schedule.getId())) { + mWaitingSchedules.put(schedule.getId(), schedule); + mHandler.removeMessages(MSG_BUILD_SCHEDULE); + mHandler.sendEmptyMessage(MSG_BUILD_SCHEDULE); + } + } + + /** + * Updates the TV input. + */ + public void updateTvInputInfo(TvInputInfo input) { + synchronized (mInputLock) { + mInput = input; + } + } + + /** + * Stops the input task scheduler. + */ + public void stop() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); + } + + private void handleStopSchedule() { + mWaitingSchedules.clear(); + int size = mPendingRecordings.size(); + for (int i = 0; i < size; ++i) { + RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; + task.cleanUp(); + } + } + + @VisibleForTesting + void handleBuildSchedule() { + if (mWaitingSchedules.isEmpty()) { + return; + } + long currentTimeMs = mClock.currentTimeMillis(); + // Remove past schedules. + for (Iterator iter = mWaitingSchedules.values().iterator(); + iter.hasNext(); ) { + ScheduledRecording schedule = iter.next(); + if (schedule.getEndTimeMs() - currentTimeMs + <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { + fail(schedule); + iter.remove(); + } + } + if (mWaitingSchedules.isEmpty()) { + return; + } + // Record the schedules which should start now. + List schedulesToStart = new ArrayList<>(); + for (ScheduledRecording schedule : mWaitingSchedules.values()) { + if (schedule.getState() != ScheduledRecording.STATE_RECORDING_CANCELED + && schedule.getStartTimeMs() - RecordingTask.RECORDING_EARLY_START_OFFSET_MS + <= currentTimeMs && schedule.getEndTimeMs() > currentTimeMs) { + schedulesToStart.add(schedule); + } + } + // The schedules will be executed with the following order. + // 1. The schedule which starts early. It can be replaced later when the schedule with the + // higher priority needs to start. + // 2. The schedule with the higher priority. It can be replaced later when the schedule with + // the higher priority needs to start. + // 3. The schedule which was created recently. + Collections.sort(schedulesToStart, getRecordingOrderComparator()); + int tunerCount; + synchronized (mInputLock) { + tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; + } + for (ScheduledRecording schedule : schedulesToStart) { + if (hasTaskWhichFinishEarlier(schedule)) { + // If there is a schedule which finishes earlier than the new schedule, rebuild the + // schedules after it finishes. + return; + } + if (mPendingRecordings.size() < tunerCount) { + // Tuners available. + createRecordingTask(schedule).start(); + mWaitingSchedules.remove(schedule.getId()); + } else { + // No available tuners. + RecordingTask task = getReplacableTask(schedule); + if (task != null) { + task.stop(); + // Just return. The schedules will be rebuilt after the task is stopped. + return; + } + } + } + if (mWaitingSchedules.isEmpty()) { + return; + } + // Set next scheduling. + long earliest = Long.MAX_VALUE; + for (ScheduledRecording schedule : mWaitingSchedules.values()) { + // The conflicting schedules will be removed if they end before conflicting resolved. + if (schedulesToStart.contains(schedule)) { + if (earliest > schedule.getEndTimeMs()) { + earliest = schedule.getEndTimeMs(); + } + } else { + if (earliest > schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { + earliest = schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; + } + } + } + mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); + } + + private RecordingTask createRecordingTask(ScheduledRecording schedule) { + Channel channel = mChannelDataManager.getChannel(schedule.getChannelId()); + RecordingTask recordingTask = mRecordingTaskFactory.createRecordingTask(schedule, channel, + mDvrManager, mSessionManager, mDataManager, mClock); + HandlerWrapper handlerWrapper = new HandlerWrapper(mLooper, schedule, recordingTask); + mPendingRecordings.put(schedule.getId(), handlerWrapper); + return recordingTask; + } + + private boolean hasTaskWhichFinishEarlier(ScheduledRecording schedule) { + int size = mPendingRecordings.size(); + for (int i = 0; i < size; ++i) { + RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; + if (task.getEndTimeMs() <= schedule.getStartTimeMs()) { + return true; + } + } + return false; + } + + private RecordingTask getReplacableTask(ScheduledRecording schedule) { + // Returns the recording with the following priority. + // 1. The recording with the lowest priority is returned. + // 2. If the priorities are the same, the recording which finishes early is returned. + // 3. If 1) and 2) are the same, the early created schedule is returned. + int size = mPendingRecordings.size(); + RecordingTask candidate = null; + for (int i = 0; i < size; ++i) { + RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; + if (schedule.getPriority() > task.getPriority()) { + if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { + candidate = task; + } + } + } + return candidate; + } + + private void fail(ScheduledRecording schedule) { + // It's called when the scheduling has been failed without creating RecordingTask. + runOnMainHandler(new Runnable() { + @Override + public void run() { + ScheduledRecording scheduleInManager = + mDataManager.getScheduledRecording(schedule.getId()); + if (scheduleInManager != null) { + // The schedule should be updated based on the object from DataManager in case + // when it has been updated. + mDataManager.changeState(scheduleInManager, + ScheduledRecording.STATE_RECORDING_FAILED); + } + } + }); + } + + private void runOnMainHandler(Runnable runnable) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + runnable.run(); + } else { + mMainThreadHandler.post(runnable); + } + } + + @VisibleForTesting + interface RecordingTaskFactory { + RecordingTask createRecordingTask(ScheduledRecording scheduledRecording, Channel channel, + DvrManager dvrManager, InputSessionManager sessionManager, + WritableDvrDataManager dataManager, Clock clock); + } + + private class WorkerThreadHandler extends Handler { + public WorkerThreadHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ADD_SCHEDULED_RECORDING: + handleAddSchedule((ScheduledRecording) msg.obj); + break; + case MSG_REMOVE_SCHEDULED_RECORDING: + handleRemoveSchedule((ScheduledRecording) msg.obj); + break; + case MSG_UPDATE_SCHEDULED_RECORDING: + handleUpdateSchedule((ScheduledRecording) msg.obj); + case MSG_BUILD_SCHEDULE: + handleBuildSchedule(); + break; + case MSG_STOP_SCHEDULE: + handleStopSchedule(); + break; + } + } + } +} diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java new file mode 100644 index 00000000..c3314dde --- /dev/null +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingClient.RecordingCallback; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.util.Log; +import android.widget.Toast; + +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.RecordingSession; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.dvr.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; + +import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +/** + * A Handler that actually starts and stop a recording at the right time. + * + *

This is run on the looper of thread named {@value DvrRecordingService#HANDLER_THREAD_NAME}. + * There is only one looper so messages must be handled quickly or start a separate thread. + */ +@WorkerThread +@VisibleForTesting +@TargetApi(Build.VERSION_CODES.N) +public class RecordingTask extends RecordingCallback implements Handler.Callback, + DvrManager.Listener { + private static final String TAG = "RecordingTask"; + private static final boolean DEBUG = false; + + /** + * Compares the end time in ascending order. + */ + public static final Comparator END_TIME_COMPARATOR + = new Comparator() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); + } + }; + + /** + * Compares ID in ascending order. + */ + public static final Comparator ID_COMPARATOR + = new Comparator() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); + } + }; + + /** + * Compares the priority in ascending order. + */ + public static final Comparator PRIORITY_COMPARATOR + = new Comparator() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getPriority(), rhs.getPriority()); + } + }; + + @VisibleForTesting + static final int MSG_INITIALIZE = 1; + @VisibleForTesting + static final int MSG_START_RECORDING = 2; + @VisibleForTesting + static final int MSG_STOP_RECORDING = 3; + /** + * Message to update schedule. + */ + public static final int MSG_UDPATE_SCHEDULE = 4; + + /** + * The time when the start command will be sent before the recording starts. + */ + public static final long RECORDING_EARLY_START_OFFSET_MS = TimeUnit.SECONDS.toMillis(3); + /** + * If the recording starts later than the scheduled start time or ends before the scheduled end + * time, it's considered as clipped. + */ + private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5); + + @VisibleForTesting + enum State { + NOT_STARTED, + SESSION_ACQUIRED, + CONNECTION_PENDING, + CONNECTED, + RECORDING_STARTED, + RECORDING_STOP_REQUESTED, + FINISHED, + ERROR, + RELEASED, + } + private final InputSessionManager mSessionManager; + private final DvrManager mDvrManager; + private final Context mContext; + + private final WritableDvrDataManager mDataManager; + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + private RecordingSession mRecordingSession; + private Handler mHandler; + private ScheduledRecording mScheduledRecording; + private final Channel mChannel; + private State mState = State.NOT_STARTED; + private final Clock mClock; + private boolean mStartedWithClipping; + private Uri mRecordedProgramUri; + private boolean mCanceled; + + RecordingTask(Context context, ScheduledRecording scheduledRecording, Channel channel, + DvrManager dvrManager, InputSessionManager sessionManager, + WritableDvrDataManager dataManager, Clock clock) { + mContext = context; + mScheduledRecording = scheduledRecording; + mChannel = channel; + mSessionManager = sessionManager; + mDataManager = dataManager; + mClock = clock; + mDvrManager = dvrManager; + + if (DEBUG) Log.d(TAG, "created recording task " + mScheduledRecording); + } + + public void setHandler(Handler handler) { + mHandler = handler; + } + + @Override + public boolean handleMessage(Message msg) { + if (DEBUG) Log.d(TAG, "handleMessage " + msg); + SoftPreconditions.checkState(msg.what == HandlerWrapper.MESSAGE_REMOVE || mHandler != null, + TAG, "Null handler trying to handle " + msg); + try { + switch (msg.what) { + case MSG_INITIALIZE: + handleInit(); + break; + case MSG_START_RECORDING: + handleStartRecording(); + break; + case MSG_STOP_RECORDING: + handleStopRecording(); + break; + case MSG_UDPATE_SCHEDULE: + handleUpdateSchedule((ScheduledRecording) msg.obj); + break; + case HandlerWrapper.MESSAGE_REMOVE: + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + release(); + return false; + default: + SoftPreconditions.checkArgument(false, TAG, "unexpected message type " + msg); + break; + } + return true; + } catch (Exception e) { + Log.w(TAG, "Error processing message " + msg + " for " + mScheduledRecording, e); + failAndQuit(); + } + return false; + } + + @Override + public void onDisconnected(String inputId) { + if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")"); + if (mRecordingSession != null && mState != State.FINISHED) { + failAndQuit(); + } + } + + @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); + if (mRecordingSession != null) { + failAndQuit(); + } + } + + @Override + public void onTuned(Uri channelUri) { + if (DEBUG) Log.d(TAG, "onTuned"); + if (mRecordingSession == null) { + return; + } + mState = State.CONNECTED; + if (mHandler == null || !sendEmptyMessageAtAbsoluteTime(MSG_START_RECORDING, + mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) { + failAndQuit(); + } + } + + @Override + public void onRecordingStopped(Uri recordedProgramUri) { + if (DEBUG) Log.d(TAG, "onRecordingStopped"); + if (mRecordingSession == null) { + return; + } + mRecordedProgramUri = recordedProgramUri; + mState = State.FINISHED; + int state = ScheduledRecording.STATE_RECORDING_FINISHED; + if (mStartedWithClipping || mScheduledRecording.getEndTimeMs() - CLIPPED_THRESHOLD_MS + > mClock.currentTimeMillis()) { + state = ScheduledRecording.STATE_RECORDING_CLIPPED; + } + updateRecordingState(state); + sendRemove(); + if (mCanceled) { + removeRecordedProgram(); + } + } + + @Override + public void onError(int reason) { + if (DEBUG) Log.d(TAG, "onError reason " + reason); + if (mRecordingSession == null) { + return; + } + switch (reason) { + case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE: + mMainThreadHandler.post(new Runnable() { + @Override + public void run() { + if (TvApplication.getSingletons(mContext).getMainActivityWrapper() + .isResumed()) { + 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)); + } + } + }); + // Pass through + default: + failAndQuit(); + break; + } + } + + private void handleInit() { + if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording); + if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) { + Log.w(TAG, "End time already past, not recording " + mScheduledRecording); + failAndQuit(); + return; + } + if (mChannel == null) { + Log.w(TAG, "Null channel for " + mScheduledRecording); + failAndQuit(); + return; + } + if (mChannel.getId() != mScheduledRecording.getChannelId()) { + Log.w(TAG, "Channel" + mChannel + " does not match scheduled recording " + + mScheduledRecording); + failAndQuit(); + return; + } + + String inputId = mChannel.getInputId(); + mRecordingSession = mSessionManager.createRecordingSession(inputId, + "recordingTask-" + mScheduledRecording.getId(), this, + mHandler, mScheduledRecording.getEndTimeMs()); + mState = State.SESSION_ACQUIRED; + mDvrManager.addListener(this, mHandler); + mRecordingSession.tune(inputId, mChannel.getUri()); + mState = State.CONNECTION_PENDING; + } + + private void failAndQuit() { + if (DEBUG) Log.d(TAG, "failAndQuit"); + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + mState = State.ERROR; + sendRemove(); + } + + private void sendRemove() { + if (DEBUG) Log.d(TAG, "sendRemove"); + if (mHandler != null) { + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage( + HandlerWrapper.MESSAGE_REMOVE)); + } + } + + private void handleStartRecording() { + if (DEBUG) Log.d(TAG, "handleStartRecording " + mScheduledRecording); + long programId = mScheduledRecording.getProgramId(); + mRecordingSession.startRecording(programId == ScheduledRecording.ID_NOT_SET ? null + : TvContract.buildProgramUri(programId)); + updateRecordingState(ScheduledRecording.STATE_RECORDING_IN_PROGRESS); + // If it starts late, it's clipped. + if (mScheduledRecording.getStartTimeMs() + CLIPPED_THRESHOLD_MS + < mClock.currentTimeMillis()) { + mStartedWithClipping = true; + } + mState = State.RECORDING_STARTED; + + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, + mScheduledRecording.getEndTimeMs())) { + failAndQuit(); + } + } + + private void handleStopRecording() { + if (DEBUG) Log.d(TAG, "handleStopRecording " + mScheduledRecording); + mRecordingSession.stopRecording(); + mState = State.RECORDING_STOP_REQUESTED; + } + + private void handleUpdateSchedule(ScheduledRecording schedule) { + mScheduledRecording = schedule; + // Check end time only. The start time is checked in InputTaskScheduler. + if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { + if (mRecordingSession != null) { + mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); + } + if (mState == State.RECORDING_STARTED) { + mHandler.removeMessages(MSG_STOP_RECORDING); + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { + failAndQuit(); + } + } + } + } + + @VisibleForTesting + State getState() { + return mState; + } + + private long getScheduleId() { + return mScheduledRecording.getId(); + } + + /** + * Returns the priority. + */ + public long getPriority() { + return mScheduledRecording.getPriority(); + } + + /** + * Returns the start time of the recording. + */ + public long getStartTimeMs() { + return mScheduledRecording.getStartTimeMs(); + } + + /** + * Returns the end time of the recording. + */ + public long getEndTimeMs() { + return mScheduledRecording.getEndTimeMs(); + } + + private void release() { + if (mRecordingSession != null) { + mSessionManager.releaseRecordingSession(mRecordingSession); + mRecordingSession = null; + } + mDvrManager.removeListener(this); + } + + private boolean sendEmptyMessageAtAbsoluteTime(int what, long when) { + long now = mClock.currentTimeMillis(); + long delay = Math.max(0L, when - now); + if (DEBUG) { + Log.d(TAG, "Sending message " + what + " with a delay of " + delay / 1000 + + " seconds to arrive at " + Utils.toIsoDateTimeString(when)); + } + return mHandler.sendEmptyMessageDelayed(what, delay); + } + + private void updateRecordingState(@ScheduledRecording.RecordingState int state) { + if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); + mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) + .build(); + runOnMainThread(new Runnable() { + @Override + public void run() { + ScheduledRecording schedule = mDataManager.getScheduledRecording( + mScheduledRecording.getId()); + if (schedule == null) { + // Schedule has been deleted. Delete the recorded program. + removeRecordedProgram(); + } else { + // Update the state based on the object in DataManager in case when it has been + // updated. mScheduledRecording will be updated from + // onScheduledRecordingStateChanged. + mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) + .setState(state).build()); + } + } + }); + } + + @Override + public void onStopRecordingRequested(ScheduledRecording recording) { + if (recording.getId() != mScheduledRecording.getId()) { + return; + } + stop(); + } + + /** + * Starts the task. + */ + public void start() { + mHandler.sendEmptyMessage(MSG_INITIALIZE); + } + + /** + * Stops the task. + */ + public void stop() { + if (DEBUG) Log.d(TAG, "stop"); + switch (mState) { + case RECORDING_STARTED: + mHandler.removeMessages(MSG_STOP_RECORDING); + handleStopRecording(); + break; + case RECORDING_STOP_REQUESTED: + // Do nothing + break; + case NOT_STARTED: + case SESSION_ACQUIRED: + case CONNECTION_PENDING: + case CONNECTED: + case FINISHED: + case ERROR: + case RELEASED: + default: + sendRemove(); + break; + } + } + + /** + * Cancels the task + */ + public void cancel() { + if (DEBUG) Log.d(TAG, "cancel"); + mCanceled = true; + stop(); + removeRecordedProgram(); + } + + /** + * Clean up the task. + */ + public void cleanUp() { + if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + } + release(); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + } + + @Override + public String toString() { + return getClass().getName() + "(" + mScheduledRecording + ")"; + } + + private void removeRecordedProgram() { + runOnMainThread(new Runnable() { + @Override + public void run() { + if (mRecordedProgramUri != null) { + mDvrManager.removeRecordedProgram(mRecordedProgramUri); + } + } + }); + } + + private void runOnMainThread(Runnable runnable) { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + mMainThreadHandler.post(runnable); + } + } +} diff --git a/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java new file mode 100644 index 00000000..d958c4a1 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.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.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; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Deletes {@link ScheduledRecording} older than {@value @DAYS} days. + */ +class ScheduledProgramReaper implements Runnable { + + @VisibleForTesting + static final int DAYS = 2; + private final WritableDvrDataManager mDvrDataManager; + private final Clock mClock; + + ScheduledProgramReaper(WritableDvrDataManager dvrDataManager, Clock clock) { + mDvrDataManager = dvrDataManager; + mClock = clock; + } + + @Override + @MainThread + public void run() { + long cutoff = mClock.currentTimeMillis() - TimeUnit.DAYS.toMillis(DAYS); + List toRemove = new ArrayList<>(); + for (ScheduledRecording r : mDvrDataManager.getAllScheduledRecordings()) { + // Do not remove the schedules if it belongs to the series recording and was finished + // successfully. The schedule is necessary for checking the scheduled episode of the + // series recording. + if (r.getEndTimeMs() < cutoff + && (r.getSeriesRecordingId() == SeriesRecording.ID_NOT_SET + || r.getState() != ScheduledRecording.STATE_RECORDING_FINISHED)) { + toRemove.add(r); + } + } + for (ScheduledRecording r : mDvrDataManager.getDeletedSchedules()) { + if (r.getEndTimeMs() < cutoff) { + toRemove.add(r); + } + } + if (!toRemove.isEmpty()) { + mDvrDataManager.removeScheduledRecording(ScheduledRecording.toArray(toRemove)); + } + } +} diff --git a/src/com/android/tv/dvr/recorder/Scheduler.java b/src/com/android/tv/dvr/recorder/Scheduler.java new file mode 100644 index 00000000..19e73342 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/Scheduler.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.recorder; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Looper; +import android.support.annotation.MainThread; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.util.Log; +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; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * The core class to manage schedule and run actual recording. + */ +@MainThread +public class Scheduler extends TvInputCallback implements ScheduledRecordingListener { + private static final String TAG = "Scheduler"; + private static final boolean DEBUG = false; + + private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5); + @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1); + + private final Looper mLooper; + private final InputSessionManager mSessionManager; + private final WritableDvrDataManager mDataManager; + private final DvrManager mDvrManager; + private final ChannelDataManager mChannelDataManager; + private final TvInputManagerHelper mInputManager; + private final Context mContext; + private final Clock mClock; + private final AlarmManager mAlarmManager; + + private final Map mInputSchedulerMap = new ArrayMap<>(); + private long mLastStartTimePendingMs; + + public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, + WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, + TvInputManagerHelper inputManager, Context context, Clock clock, + AlarmManager alarmManager) { + mLooper = looper; + mDvrManager = dvrManager; + mSessionManager = sessionManager; + mDataManager = dataManager; + mChannelDataManager = channelDataManager; + mInputManager = inputManager; + mContext = context; + mClock = clock; + mAlarmManager = alarmManager; + } + + /** + * Starts the scheduler. + */ + public void start() { + mDataManager.addScheduledRecordingListener(this); + mInputManager.addCallback(this); + if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { + updateInternal(); + } else { + if (!mDataManager.isDvrScheduleLoadFinished()) { + mDataManager.addDvrScheduleLoadFinishedListener( + new OnDvrScheduleLoadFinishedListener() { + @Override + public void onDvrScheduleLoadFinished() { + mDataManager.removeDvrScheduleLoadFinishedListener(this); + updateInternal(); + } + }); + } + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener(new Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + updateInternal(); + } + + @Override + public void onChannelListUpdated() { } + + @Override + public void onChannelBrowsableChanged() { } + }); + } + } + } + + /** + * Stops the scheduler. + */ + public void stop() { + for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { + inputTaskScheduler.stop(); + } + mInputManager.removeCallback(this); + mDataManager.removeScheduledRecordingListener(this); + } + + private void updatePendingRecordings() { + List scheduledRecordings = mDataManager + .getScheduledRecordings(new Range<>(mLastStartTimePendingMs, + mClock.currentTimeMillis() + SOON_DURATION_IN_MS), + ScheduledRecording.STATE_RECORDING_NOT_STARTED); + for (ScheduledRecording r : scheduledRecordings) { + scheduleRecordingSoon(r); + } + } + + /** + * Start recording that will happen soon, and set the next alarm time. + */ + public void update() { + if (DEBUG) Log.d(TAG, "update"); + updateInternal(); + } + + private void updateInternal() { + if (isInitialized()) { + updatePendingRecordings(); + updateNextAlarm(); + } + } + + private boolean isInitialized() { + return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { + if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules)); + if (!isInitialized()) { + return; + } + handleScheduleChange(schedules); + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules)); + if (!isInitialized()) { + return; + } + boolean needToUpdateAlarm = false; + for (ScheduledRecording schedule : schedules) { + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); + if (scheduler != null) { + scheduler.removeSchedule(schedule); + needToUpdateAlarm = true; + } + } + if (needToUpdateAlarm) { + updateNextAlarm(); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules)); + if (!isInitialized()) { + return; + } + // Update the recordings. + for (ScheduledRecording schedule : schedules) { + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); + if (scheduler != null) { + scheduler.updateSchedule(schedule); + } + } + handleScheduleChange(schedules); + } + + private void handleScheduleChange(ScheduledRecording... schedules) { + boolean needToUpdateAlarm = false; + for (ScheduledRecording schedule : schedules) { + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + if (startsWithin(schedule, SOON_DURATION_IN_MS)) { + scheduleRecordingSoon(schedule); + } else { + needToUpdateAlarm = true; + } + } + } + if (needToUpdateAlarm) { + updateNextAlarm(); + } + } + + private void scheduleRecordingSoon(ScheduledRecording schedule) { + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (input == null) { + Log.e(TAG, "Can't find input for " + schedule); + mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); + return; + } + if (!input.canRecord() || input.getTunerCount() <= 0) { + Log.e(TAG, "TV input doesn't support recording: " + input); + mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); + return; + } + InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + if (scheduler == null) { + scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager, + mDvrManager, mDataManager, mSessionManager, mClock); + mInputSchedulerMap.put(input.getId(), scheduler); + } + scheduler.addSchedule(schedule); + if (mLastStartTimePendingMs < schedule.getStartTimeMs()) { + mLastStartTimePendingMs = schedule.getStartTimeMs(); + } + } + + private void updateNextAlarm() { + long nextStartTime = mDataManager.getNextScheduledStartTimeAfter( + Math.max(mLastStartTimePendingMs, mClock.currentTimeMillis())); + if (nextStartTime != DvrDataManager.NEXT_START_TIME_NOT_FOUND) { + long wakeAt = nextStartTime - MS_TO_WAKE_BEFORE_START; + if (DEBUG) Log.d(TAG, "Set alarm to record at " + wakeAt); + Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); + PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); + // This will cancel the previous alarm. + mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); + } else { + if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); + } + } + + @VisibleForTesting + boolean startsWithin(ScheduledRecording scheduledRecording, long durationInMs) { + return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; + } + + // No need to remove input task scheduler when the input is removed. If the input is removed + // temporarily, the scheduler should keep the non-started schedules. + @Override + public void onInputUpdated(String inputId) { + InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId); + if (scheduler != null) { + scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); + } + } + + @Override + public void onTvInputInfoUpdated(TvInputInfo input) { + InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + if (scheduler != null) { + scheduler.updateTvInputInfo(input); + } + } +} diff --git a/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java new file mode 100644 index 00000000..8a211f66 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java @@ -0,0 +1,562 @@ +/* + * 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.recorder; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Build; +import android.support.annotation.MainThread; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.LongSparseArray; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.TvApplication; +import com.android.tv.common.CollectionUtils; +import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Program; +import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +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; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for + * the {@link com.android.tv.dvr.data.SeriesRecording}. + *

+ * The current implementation assumes that the series recordings are scheduled only for one channel. + */ +@TargetApi(Build.VERSION_CODES.N) +public class SeriesRecordingScheduler { + private static final String TAG = "SeriesRecordingSchd"; + private static final boolean DEBUG = false; + + private static final String KEY_FETCHED_SERIES_IDS = + "SeriesRecordingScheduler.fetched_series_ids"; + + @SuppressLint("StaticFieldLeak") + private static SeriesRecordingScheduler sInstance; + + /** + * Creates and returns the {@link SeriesRecordingScheduler}. + */ + public static synchronized SeriesRecordingScheduler getInstance(Context context) { + if (sInstance == null) { + sInstance = new SeriesRecordingScheduler(context); + } + return sInstance; + } + + private final Context mContext; + private final DvrManager mDvrManager; + private final WritableDvrDataManager mDataManager; + private final List mScheduleTasks = new ArrayList<>(); + private final LongSparseArray mFetchSeriesInfoTasks = + new LongSparseArray<>(); + private final Set mFetchedSeriesIds = new ArraySet<>(); + private final SharedPreferences mSharedPreferences; + private boolean mStarted; + private boolean mPaused; + private final Set mPendingSeriesRecordings = new ArraySet<>(); + + private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + executeFetchSeriesInfoTask(seriesRecording); + } + } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + // Cancel the update. + for (Iterator iter = mScheduleTasks.iterator(); + iter.hasNext(); ) { + SeriesRecordingUpdateTask task = iter.next(); + if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, + SeriesRecording.ID_COMPARATOR).isEmpty()) { + task.cancel(true); + iter.remove(); + } + } + for (SeriesRecording seriesRecording : seriesRecordings) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId()); + if (task != null) { + task.cancel(true); + mFetchSeriesInfoTasks.remove(seriesRecording.getId()); + } + } + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + List stopped = new ArrayList<>(); + List normal = new ArrayList<>(); + for (SeriesRecording r : seriesRecordings) { + if (r.isStopped()) { + stopped.add(r); + } else { + normal.add(r); + } + } + if (!stopped.isEmpty()) { + onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); + } + if (!normal.isEmpty()) { + updateSchedules(normal); + } + } + }; + + private final ScheduledRecordingListener mScheduledRecordingListener = + new ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { + // No need to update series recordings when the new schedule is added. + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + handleScheduledRecordingChange(Arrays.asList(schedules)); + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + List schedulesForUpdate = new ArrayList<>(); + for (ScheduledRecording r : schedules) { + if ((r.getState() == ScheduledRecording.STATE_RECORDING_FAILED + || r.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED) + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && !TextUtils.isEmpty(r.getSeasonNumber()) + && !TextUtils.isEmpty(r.getEpisodeNumber())) { + schedulesForUpdate.add(r); + } + } + if (!schedulesForUpdate.isEmpty()) { + handleScheduledRecordingChange(schedulesForUpdate); + } + } + + private void handleScheduledRecordingChange(List schedules) { + if (schedules.isEmpty()) { + return; + } + Set seriesRecordingIds = new HashSet<>(); + for (ScheduledRecording r : schedules) { + if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + seriesRecordingIds.add(r.getSeriesRecordingId()); + } + } + if (!seriesRecordingIds.isEmpty()) { + List seriesRecordings = new ArrayList<>(); + for (Long id : seriesRecordingIds) { + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(id); + if (seriesRecording != null) { + seriesRecordings.add(seriesRecording); + } + } + if (!seriesRecordings.isEmpty()) { + updateSchedules(seriesRecordings); + } + } + } + }; + + private SeriesRecordingScheduler(Context context) { + mContext = context.getApplicationContext(); + ApplicationSingletons appSingletons = TvApplication.getSingletons(context); + mDvrManager = appSingletons.getDvrManager(); + mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); + mSharedPreferences = context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_SERIES_RECORDINGS, Context.MODE_PRIVATE); + mFetchedSeriesIds.addAll(mSharedPreferences.getStringSet(KEY_FETCHED_SERIES_IDS, + Collections.emptySet())); + } + + /** + * Starts the scheduler. + */ + @MainThread + public void start() { + SoftPreconditions.checkState(mDataManager.isInitialized()); + if (mStarted) { + return; + } + if (DEBUG) Log.d(TAG, "start"); + mStarted = true; + mDataManager.addSeriesRecordingListener(mSeriesRecordingListener); + mDataManager.addScheduledRecordingListener(mScheduledRecordingListener); + startFetchingSeriesInfo(); + updateSchedules(mDataManager.getSeriesRecordings()); + } + + @MainThread + public void stop() { + if (!mStarted) { + return; + } + if (DEBUG) Log.d(TAG, "stop"); + mStarted = false; + for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i)); + task.cancel(true); + } + mFetchSeriesInfoTasks.clear(); + for (SeriesRecordingUpdateTask task : mScheduleTasks) { + task.cancel(true); + } + mScheduleTasks.clear(); + mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); + mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); + } + + private void startFetchingSeriesInfo() { + for (SeriesRecording seriesRecording : mDataManager.getSeriesRecordings()) { + if (!mFetchedSeriesIds.contains(seriesRecording.getSeriesId())) { + executeFetchSeriesInfoTask(seriesRecording); + } + } + } + + private void executeFetchSeriesInfoTask(SeriesRecording seriesRecording) { + if (Experiments.CLOUD_EPG.get()) { + FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); + task.execute(); + mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); + } + } + + /** + * Pauses the updates of the series recordings. + */ + public void pauseUpdate() { + if (DEBUG) Log.d(TAG, "Schedule paused"); + if (mPaused) { + return; + } + mPaused = true; + if (!mStarted) { + return; + } + for (SeriesRecordingUpdateTask task : mScheduleTasks) { + for (SeriesRecording r : task.getSeriesRecordings()) { + mPendingSeriesRecordings.add(r.getId()); + } + task.cancel(true); + } + } + + /** + * Resumes the updates of the series recordings. + */ + public void resumeUpdate() { + if (DEBUG) Log.d(TAG, "Schedule resumed"); + if (!mPaused) { + return; + } + mPaused = false; + if (!mStarted) { + return; + } + if (!mPendingSeriesRecordings.isEmpty()) { + List seriesRecordings = new ArrayList<>(); + for (long seriesRecordingId : mPendingSeriesRecordings) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(seriesRecordingId); + if (seriesRecording != null) { + seriesRecordings.add(seriesRecording); + } + } + if (!seriesRecordings.isEmpty()) { + updateSchedules(seriesRecordings); + } + } + } + + /** + * Update schedules for the given series recordings. If it's paused, the update will be done + * after it's resumed. + */ + public void updateSchedules(Collection seriesRecordings) { + if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings); + if (!mStarted) { + if (DEBUG) Log.d(TAG, "Not started yet."); + return; + } + if (mPaused) { + for (SeriesRecording r : seriesRecordings) { + mPendingSeriesRecordings.add(r.getId()); + } + if (DEBUG) { + Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size=" + + mPendingSeriesRecordings.size()); + } + return; + } + Set previousSeriesRecordings = new HashSet<>(); + for (Iterator iter = mScheduleTasks.iterator(); + iter.hasNext(); ) { + SeriesRecordingUpdateTask task = iter.next(); + if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings, + SeriesRecording.ID_COMPARATOR)) { + // The task is affected by the seriesRecordings + task.cancel(true); + previousSeriesRecordings.addAll(task.getSeriesRecordings()); + iter.remove(); + } + } + List seriesRecordingsToUpdate = CollectionUtils.union(seriesRecordings, + previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); + for (Iterator iter = seriesRecordingsToUpdate.iterator(); + iter.hasNext(); ) { + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); + if (seriesRecording == null || seriesRecording.isStopped()) { + // Series recording has been removed or stopped. + iter.remove(); + } + } + if (seriesRecordingsToUpdate.isEmpty()) { + return; + } + if (needToReadAllChannels(seriesRecordingsToUpdate)) { + SeriesRecordingUpdateTask task = + new SeriesRecordingUpdateTask(seriesRecordingsToUpdate); + mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); + } else { + for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { + SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask( + Collections.singletonList(seriesRecording)); + mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); + } + } + } + + private boolean needToReadAllChannels(List seriesRecordingsToUpdate) { + for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { + if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { + return true; + } + } + return false; + } + + /** + * Pick one program per an episode. + * + *

Note that the programs which has been already scheduled have the highest priority, and all + * of them are added even though they are the same episodes. That's because the schedules + * should be added to the series recording. + *

If there are no existing schedules for an episode, one program which starts earlier is + * picked. + */ + private LongSparseArray> pickOneProgramPerEpisode( + List seriesRecordings, List programs) { + return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); + } + + /** + * @see #pickOneProgramPerEpisode(List, List) + */ + public static LongSparseArray> pickOneProgramPerEpisode( + DvrDataManager dataManager, List seriesRecordings, + List programs) { + // Initialize. + LongSparseArray> result = new LongSparseArray<>(); + Map seriesRecordingIds = new HashMap<>(); + for (SeriesRecording seriesRecording : seriesRecordings) { + result.put(seriesRecording.getId(), new ArrayList<>()); + seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); + } + // Group programs by the episode. + Map> programsForEpisodeMap = new HashMap<>(); + for (Program program : programs) { + long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); + if (TextUtils.isEmpty(program.getSeasonNumber()) + || TextUtils.isEmpty(program.getEpisodeNumber())) { + // Add all the programs if it doesn't have season number or episode number. + result.get(seriesRecordingId).add(program); + continue; + } + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, + program.getSeasonNumber(), program.getEpisodeNumber()); + List programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); + if (programsForEpisode == null) { + programsForEpisode = new ArrayList<>(); + programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); + } + programsForEpisode.add(program); + } + // Pick one program. + for (Entry> entry : programsForEpisodeMap.entrySet()) { + List programsForEpisode = entry.getValue(); + Collections.sort(programsForEpisode, new Comparator() { + @Override + public int compare(Program lhs, Program rhs) { + // Place the existing schedule first. + boolean lhsScheduled = isProgramScheduled(dataManager, lhs); + boolean rhsScheduled = isProgramScheduled(dataManager, rhs); + if (lhsScheduled && !rhsScheduled) { + return -1; + } + if (!lhsScheduled && rhsScheduled) { + return 1; + } + // Sort by the start time in ascending order. + return lhs.compareTo(rhs); + } + }); + boolean added = false; + // Add all the scheduled programs + List programsForSeries = result.get(entry.getKey().seriesRecordingId); + for (Program program : programsForEpisode) { + if (isProgramScheduled(dataManager, program)) { + programsForSeries.add(program); + added = true; + } else if (!added) { + programsForSeries.add(program); + break; + } + } + } + return result; + } + + private static boolean isProgramScheduled(DvrDataManager dataManager, Program program) { + ScheduledRecording schedule = + dataManager.getScheduledRecordingForProgramId(program.getId()); + return schedule != null && schedule.getState() + == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + private void updateFetchedSeries() { + mSharedPreferences.edit().putStringSet(KEY_FETCHED_SERIES_IDS, mFetchedSeriesIds).apply(); + } + + /** + * This works only for the existing series recordings. Do not use this task for the + * "adding series recording" UI. + */ + private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { + SeriesRecordingUpdateTask(List seriesRecordings) { + super(mContext, seriesRecordings); + } + + @Override + protected void onPostExecute(List programs) { + if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); + mScheduleTasks.remove(this); + if (programs == null) { + Log.e(TAG, "Creating schedules for series recording failed: " + + getSeriesRecordings()); + return; + } + LongSparseArray> seriesProgramMap = pickOneProgramPerEpisode( + getSeriesRecordings(), programs); + for (SeriesRecording seriesRecording : getSeriesRecordings()) { + // Check the series recording is still valid. + SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( + seriesRecording.getId()); + if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { + continue; + } + List programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); + if (mDataManager.getSeriesRecording(seriesRecording.getId()) != null + && !programsToSchedule.isEmpty()) { + mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); + } + } + } + + @Override + protected void onCancelled(List programs) { + mScheduleTasks.remove(this); + } + + @Override + public String toString() { + return "SeriesRecordingUpdateTask:{" + + "series_recordings=" + getSeriesRecordings() + + "}"; + } + } + + private class FetchSeriesInfoTask extends AsyncTask { + private SeriesRecording mSeriesRecording; + + FetchSeriesInfoTask(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; + } + + @Override + protected SeriesInfo doInBackground(Void... voids) { + return EpgFetcher.createEpgReader(mContext) + .getSeriesInfo(mSeriesRecording.getSeriesId()); + } + + @Override + protected void onPostExecute(SeriesInfo seriesInfo) { + if (seriesInfo != null) { + mDataManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) + .setTitle(seriesInfo.getTitle()) + .setDescription(seriesInfo.getDescription()) + .setLongDescription(seriesInfo.getLongDescription()) + .setCanonicalGenreIds(seriesInfo.getCanonicalGenreIds()) + .setPosterUri(seriesInfo.getPosterUri()) + .setPhotoUri(seriesInfo.getPhotoUri()) + .build()); + mFetchedSeriesIds.add(seriesInfo.getId()); + updateFetchedSeries(); + } + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + } + + @Override + protected void onCancelled(SeriesInfo seriesInfo) { + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); + } + } +} diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/ActionPresenterSelector.java deleted file mode 100644 index 8b8cd5c5..00000000 --- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java +++ /dev/null @@ -1,138 +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.graphics.drawable.Drawable; -import android.support.v17.leanback.R; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.PresenterSelector; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -// This class is adapted from Leanback's library, which does not support action icon with one-line -// label. This class modified its getPresenter method to support the above situation. -class ActionPresenterSelector extends PresenterSelector { - private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); - private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); - private final Presenter[] mPresenters = new Presenter[] { - mOneLineActionPresenter, mTwoLineActionPresenter}; - - @Override - public Presenter getPresenter(Object item) { - Action action = (Action) item; - if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) { - return mOneLineActionPresenter; - } else { - return mTwoLineActionPresenter; - } - } - - @Override - public Presenter[] getPresenters() { - return mPresenters; - } - - static class ActionViewHolder extends Presenter.ViewHolder { - Action mAction; - Button mButton; - int mLayoutDirection; - - public ActionViewHolder(View view, int layoutDirection) { - super(view); - mButton = (Button) view.findViewById(R.id.lb_action_button); - mLayoutDirection = layoutDirection; - } - } - - class OneLineActionPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_1_line, parent, false); - return new ActionViewHolder(v, parent.getLayoutDirection()); - } - - @Override - public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - Action action = (Action) item; - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mAction = action; - vh.mButton.setText(action.getLabel1()); - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ((ActionViewHolder) viewHolder).mAction = null; - } - } - - class TwoLineActionPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.lb_action_2_lines, parent, false); - return new ActionViewHolder(v, parent.getLayoutDirection()); - } - - @Override - public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - Action action = (Action) item; - ActionViewHolder vh = (ActionViewHolder) viewHolder; - Drawable icon = action.getIcon(); - vh.mAction = action; - - if (icon != null) { - final int startPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); - final int endPadding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); - vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); - } else { - final int padding = vh.view.getResources() - .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); - vh.view.setPaddingRelative(padding, 0, padding, 0); - } - if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); - } else { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - } - - CharSequence line1 = action.getLabel1(); - CharSequence line2 = action.getLabel2(); - if (TextUtils.isEmpty(line1)) { - vh.mButton.setText(line2); - } else if (TextUtils.isEmpty(line2)) { - vh.mButton.setText(line1); - } else { - vh.mButton.setText(line1 + "\n" + line2); - } - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { - ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); - vh.view.setPadding(0, 0, 0, 0); - vh.mAction = null; - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/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 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 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/DetailsContent.java b/src/com/android/tv/dvr/ui/DetailsContent.java deleted file mode 100644 index 19521fca..00000000 --- a/src/com/android/tv/dvr/ui/DetailsContent.java +++ /dev/null @@ -1,207 +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.media.tv.TvContract; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; - -/** - * A class for details content. - */ -public class DetailsContent { - /** Constant for invalid time. */ - public static final long INVALID_TIME = -1; - - private CharSequence mTitle; - private long mStartTimeUtcMillis; - private long mEndTimeUtcMillis; - private String mDescription; - private String mLogoImageUri; - private String mBackgroundImageUri; - - private DetailsContent() { } - - /** - * Returns title. - */ - public CharSequence getTitle() { - return mTitle; - } - - /** - * Returns start time. - */ - public long getStartTimeUtcMillis() { - return mStartTimeUtcMillis; - } - - /** - * Returns end time. - */ - public long getEndTimeUtcMillis() { - return mEndTimeUtcMillis; - } - - /** - * Returns description. - */ - public String getDescription() { - return mDescription; - } - - /** - * Returns Logo image URI as a String. - */ - public String getLogoImageUri() { - return mLogoImageUri; - } - - /** - * Returns background image URI as a String. - */ - public String getBackgroundImageUri() { - return mBackgroundImageUri; - } - - /** - * Copies other details content. - */ - public void copyFrom(DetailsContent other) { - if (this == other) { - return; - } - mTitle = other.mTitle; - mStartTimeUtcMillis = other.mStartTimeUtcMillis; - mEndTimeUtcMillis = other.mEndTimeUtcMillis; - mDescription = other.mDescription; - mLogoImageUri = other.mLogoImageUri; - mBackgroundImageUri = other.mBackgroundImageUri; - } - - /** - * A class for building details content. - */ - public static final class Builder { - private final DetailsContent mDetailsContent; - - public Builder() { - mDetailsContent = new DetailsContent(); - mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; - mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; - } - - /** - * Sets title. - */ - public Builder setTitle(CharSequence title) { - mDetailsContent.mTitle = title; - return this; - } - - /** - * Sets start time. - */ - public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { - mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; - return this; - } - - /** - * Sets end time. - */ - public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { - mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; - return this; - } - - /** - * Sets description. - */ - public Builder setDescription(String description) { - mDetailsContent.mDescription = description; - return this; - } - - /** - * Sets logo image URI as a String. - */ - public Builder setLogoImageUri(String logoImageUri) { - mDetailsContent.mLogoImageUri = logoImageUri; - return this; - } - - /** - * Sets background image URI as a String. - */ - public Builder setBackgroundImageUri(String backgroundImageUri) { - mDetailsContent.mBackgroundImageUri = backgroundImageUri; - return this; - } - - /** - * Sets background image and logo image URI from program and channel. - */ - public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) { - if (program != null) { - return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel); - } else { - return setImageUris(null, null, channel); - } - } - - /** - * Sets background image and logo image URI and channel is used for fallback images. - */ - public Builder setImageUris(@Nullable String posterArtUri, - @Nullable String thumbnailUri, @Nullable Channel channel) { - mDetailsContent.mLogoImageUri = null; - mDetailsContent.mBackgroundImageUri = null; - if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) { - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } else if (!TextUtils.isEmpty(posterArtUri)) { - // thumbnailUri is empty - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = posterArtUri; - } else if (!TextUtils.isEmpty(thumbnailUri)) { - // posterArtUri is empty - mDetailsContent.mLogoImageUri = thumbnailUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } - if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { - String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) - .toString(); - mDetailsContent.mLogoImageUri = channelLogoUri; - mDetailsContent.mBackgroundImageUri = channelLogoUri; - } - return this; - } - - /** - * Builds details content. - */ - public DetailsContent build() { - DetailsContent detailsContent = new DetailsContent(); - detailsContent.copyFrom(mDetailsContent); - return detailsContent; - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java deleted file mode 100644 index 175f05bc..00000000 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ /dev/null @@ -1,300 +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.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.graphics.Paint; -import android.graphics.Paint.FontMetricsInt; -import android.support.v17.leanback.widget.Presenter; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.LinearLayout; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.ui.ViewUtils; -import com.android.tv.util.Utils; - -/** - * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. - * Most codes of this class is originated from - * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. - * The latter class are re-used to provide a customized version of - * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. - */ -public class DetailsContentPresenter extends Presenter { - /** - * The ViewHolder for the {@link DetailsContentPresenter}. - */ - public static class ViewHolder extends Presenter.ViewHolder { - final TextView mTitle; - final TextView mSubtitle; - final LinearLayout mDescriptionContainer; - final TextView mBody; - final TextView mReadMoreView; - final int mTitleMargin; - final int mUnderTitleBaselineMargin; - final int mUnderSubtitleBaselineMargin; - final int mTitleLineSpacing; - final int mBodyLineSpacing; - final int mBodyMaxLines; - final int mBodyMinLines; - final FontMetricsInt mTitleFontMetricsInt; - final FontMetricsInt mSubtitleFontMetricsInt; - final FontMetricsInt mBodyFontMetricsInt; - final int mTitleMaxLines; - - private Activity mActivity; - private boolean mFullTextMode; - private int mFullTextAnimationDuration; - private boolean mIsListeningToPreDraw; - - private ViewTreeObserver.OnPreDrawListener mPreDrawListener = - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - if (mSubtitle.getVisibility() == View.VISIBLE - && mSubtitle.getTop() > view.getHeight() - && mTitle.getLineCount() > 1) { - mTitle.setMaxLines(mTitle.getLineCount() - 1); - return false; - } - final int bodyLines = mBody.getLineCount(); - final int maxLines = mFullTextMode ? bodyLines : - (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); - if (bodyLines > maxLines) { - mReadMoreView.setVisibility(View.VISIBLE); - mDescriptionContainer.setFocusable(true); - mDescriptionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mFullTextMode = true; - mReadMoreView.setVisibility(View.GONE); - mDescriptionContainer.setFocusable(false); - mDescriptionContainer.setOnClickListener(null); - mBody.setMaxLines(bodyLines); - // Minus 1 from line difference to eliminate the space - // originally occupied by "READ MORE" - showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); - } - }); - } - if (mBody.getMaxLines() != maxLines) { - mBody.setMaxLines(maxLines); - return false; - } else { - removePreDrawListener(); - return true; - } - } - }; - - public ViewHolder(final View view) { - super(view); - mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); - mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); - mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); - mDescriptionContainer = - (LinearLayout) view.findViewById(R.id.dvr_details_description_container); - mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); - - FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); - final int titleAscent = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_baseline); - // Ascent is negative - mTitleMargin = titleAscent + titleFontMetricsInt.ascent; - - mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_title_baseline_margin); - mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_subtitle_baseline_margin); - - mTitleLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_title_line_spacing); - mBodyLineSpacing = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_body_line_spacing); - - mBodyMaxLines = view.getResources().getInteger( - R.integer.lb_details_description_body_max_lines); - mBodyMinLines = view.getResources().getInteger( - R.integer.lb_details_description_body_min_lines); - mTitleMaxLines = mTitle.getMaxLines(); - - mTitleFontMetricsInt = getFontMetricsInt(mTitle); - mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); - mBodyFontMetricsInt = getFontMetricsInt(mBody); - } - - void addPreDrawListener() { - if (!mIsListeningToPreDraw) { - mIsListeningToPreDraw = true; - view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); - } - } - - void removePreDrawListener() { - if (mIsListeningToPreDraw) { - view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); - mIsListeningToPreDraw = false; - } - } - - public TextView getTitle() { - return mTitle; - } - - public TextView getSubtitle() { - return mSubtitle; - } - - public TextView getBody() { - return mBody; - } - - private FontMetricsInt getFontMetricsInt(TextView textView) { - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setTextSize(textView.getTextSize()); - paint.setTypeface(textView.getTypeface()); - return paint.getFontMetricsInt(); - } - - private void showFullText(int heightDiff) { - final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); - int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); - Animator expandAnimator = ViewUtils.createHeightAnimator( - detailsFrame, nowHeight, nowHeight + heightDiff); - expandAnimator.setDuration(mFullTextAnimationDuration); - Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - 0f, -(heightDiff / 2))); - shiftAnimator.setDuration(mFullTextAnimationDuration); - AnimatorSet fullTextAnimator = new AnimatorSet(); - fullTextAnimator.playTogether(expandAnimator, shiftAnimator); - fullTextAnimator.start(); - } - } - - private final Activity mActivity; - private final int mFullTextAnimationDuration; - - public DetailsContentPresenter(Activity activity) { - super(); - mActivity = activity; - mFullTextAnimationDuration = mActivity.getResources() - .getInteger(R.integer.dvr_details_full_text_animation_duration); - } - - @Override - public final ViewHolder onCreateViewHolder(ViewGroup parent) { - View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.dvr_details_description, parent, false); - return new ViewHolder(v); - } - - @Override - public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { - final ViewHolder vh = (ViewHolder) viewHolder; - final DetailsContent detailsContent = (DetailsContent) item; - - vh.mActivity = mActivity; - vh.mFullTextAnimationDuration = mFullTextAnimationDuration; - - boolean hasTitle = true; - if (TextUtils.isEmpty(detailsContent.getTitle())) { - vh.mTitle.setVisibility(View.GONE); - hasTitle = false; - } else { - vh.mTitle.setText(detailsContent.getTitle()); - vh.mTitle.setVisibility(View.VISIBLE); - vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() - + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); - vh.mTitle.setMaxLines(vh.mTitleMaxLines); - } - setTopMargin(vh.mTitle, vh.mTitleMargin); - - boolean hasSubtitle = true; - if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME - && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { - vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), - detailsContent.getStartTimeUtcMillis(), - detailsContent.getEndTimeUtcMillis(), false)); - vh.mSubtitle.setVisibility(View.VISIBLE); - if (hasTitle) { - setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin - + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); - } else { - setTopMargin(vh.mSubtitle, 0); - } - } else { - vh.mSubtitle.setVisibility(View.GONE); - hasSubtitle = false; - } - - if (TextUtils.isEmpty(detailsContent.getDescription())) { - vh.mBody.setVisibility(View.GONE); - } else { - vh.mBody.setText(detailsContent.getDescription()); - vh.mBody.setVisibility(View.VISIBLE); - vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() - + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); - if (hasSubtitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); - } else if (hasTitle) { - setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin - + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent - - vh.mBody.getPaddingTop()); - } else { - setTopMargin(vh.mDescriptionContainer, 0); - } - } - } - - @Override - public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } - - @Override - public void onViewAttachedToWindow(Presenter.ViewHolder holder) { - // In case predraw listener was removed in detach, make sure - // we have the proper layout. - ViewHolder vh = (ViewHolder) holder; - vh.addPreDrawListener(); - super.onViewAttachedToWindow(holder); - } - - @Override - public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { - ViewHolder vh = (ViewHolder) holder; - vh.removePreDrawListener(); - super.onViewDetachedFromWindow(holder); - } - - private void setTopMargin(View view, int topMargin) { - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); - lp.topMargin = topMargin; - view.setLayoutParams(lp); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java deleted file mode 100644 index 6714ecd3..00000000 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ /dev/null @@ -1,92 +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.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.support.v17.leanback.app.BackgroundManager; - -/** - * The Background Helper. - */ -public class DetailsViewBackgroundHelper { - // Background delay serves to avoid kicking off expensive bitmap loading - // in case multiple backgrounds are set in quick succession. - private static final int SET_BACKGROUND_DELAY_MS = 100; - - private final BackgroundManager mBackgroundManager; - - class LoadBackgroundRunnable implements Runnable { - final Drawable mBackGround; - - LoadBackgroundRunnable(Drawable background) { - mBackGround = background; - } - - @Override - public void run() { - if (!mBackgroundManager.isAttached()) { - return; - } - if (mBackGround instanceof BitmapDrawable) { - mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap()); - } - mRunnable = null; - } - } - - private LoadBackgroundRunnable mRunnable; - - private final Handler mHandler = new Handler(); - - public DetailsViewBackgroundHelper(Activity activity) { - mBackgroundManager = BackgroundManager.getInstance(activity); - mBackgroundManager.attach(activity.getWindow()); - } - - /** - * Sets the given image to background. - */ - public void setBackground(Drawable background) { - if (mRunnable != null) { - mHandler.removeCallbacks(mRunnable); - } - mRunnable = new LoadBackgroundRunnable(background); - mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS); - } - - /** - * Sets the background color. - */ - public void setBackgroundColor(int color) { - if (mBackgroundManager.isAttached()) { - mBackgroundManager.setColor(color); - } - } - - /** - * Sets the background scrim. - */ - public void setScrim(int color) { - if (mBackgroundManager.isAttached()) { - mBackgroundManager.setDimLayer(new ColorDrawable(color)); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/DvrActivity.java deleted file mode 100644 index 45fb1cf1..00000000 --- a/src/com/android/tv/dvr/ui/DvrActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; - -import com.android.tv.R; -import com.android.tv.TvApplication; - -/** - * {@link android.app.Activity} for DVR UI. - */ -public class DvrActivity extends Activity { - @Override - public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - super.onCreate(savedInstanceState); - setContentView(R.layout.dvr_main); - } -} diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java 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/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java deleted file mode 100644 index a6dd31d1..00000000 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ /dev/null @@ -1,601 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.media.tv.TvInputManager.TvInputCallback; -import android.os.Bundle; -import android.os.Handler; -import android.support.v17.leanback.app.BrowseFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.Presenter; -import android.support.v17.leanback.widget.TitleViewAdapter; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.GenreItems; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.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 java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; - -/** - * {@link BrowseFragment} for DVR functions. - */ -public class DvrBrowseFragment extends BrowseFragment implements - RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener, - OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener { - private static final String TAG = "DvrBrowseFragment"; - private static final boolean DEBUG = false; - - private static final int MAX_RECENT_ITEM_COUNT = 10; - private static final int MAX_SCHEDULED_ITEM_COUNT = 4; - - private RecordedProgramAdapter mRecentAdapter; - private ScheduleAdapter mScheduleAdapter; - private SeriesAdapter mSeriesAdapter; - private RecordedProgramAdapter[] mGenreAdapters = - new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; - private ListRow mRecentRow; - private ListRow mSeriesRow; - private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; - private List mGenreLabels; - private DvrDataManager mDvrDataManager; - private DvrScheduleManager mDvrScheudleManager; - private ArrayObjectAdapter mRowsAdapter; - private ClassPresenterSelector mPresenterSelector; - private final HashMap mSeriesId2LatestProgram = new HashMap<>(); - private final Handler mHandler = new Handler(); - - private final Comparator RECORDED_PROGRAM_COMPARATOR = new Comparator() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof SeriesRecording) { - lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); - } - if (rhs instanceof SeriesRecording) { - rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); - } - if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else { - return -1; - } - } else if (rhs instanceof RecordedProgram) { - return 1; - } else { - return 0; - } - } - }; - - private final Comparator SCHEDULE_COMPARATOR = new Comparator() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else { - return -1; - } - } else if (rhs instanceof ScheduledRecording) { - return 1; - } else { - return 0; - } - } - }; - - private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = - new DvrScheduleManager.OnConflictStateChangeListener() { - @Override - public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { - if (mScheduleAdapter != null) { - for (ScheduledRecording schedule : schedules) { - onScheduledRecordingStatusChanged(schedule); - } - } - } - }; - - private final Runnable mUpdateRowsRunnable = new Runnable() { - @Override - public void run() { - updateRows(); - } - }; - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - Context context = getContext(); - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mDvrDataManager = singletons.getDvrDataManager(); - mDvrScheudleManager = singletons.getDvrScheduleManager(); - mPresenterSelector = new ClassPresenterSelector() - .addClassPresenter(ScheduledRecording.class, - new ScheduledRecordingPresenter(context)) - .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) - .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter()); - mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); - mGenreLabels.add(getString(R.string.dvr_main_others)); - setupUiElements(); - setupAdapters(); - mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); - prepareEntranceTransition(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } else { - if (!mDvrDataManager.isDvrScheduleLoadFinished()) { - mDvrDataManager.addDvrScheduleLoadFinishedListener(this); - } - if (!mDvrDataManager.isRecordedProgramLoadFinished()) { - mDvrDataManager.addRecordedProgramLoadFinishedListener(this); - } - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mHandler.removeCallbacks(mUpdateRowsRunnable); - mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); - mDvrDataManager.removeRecordedProgramListener(this); - mDvrDataManager.removeScheduledRecordingListener(this); - mDvrDataManager.removeSeriesRecordingListener(this); - mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); - mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); - mRowsAdapter.clear(); - mSeriesId2LatestProgram.clear(); - for (Presenter presenter : mPresenterSelector.getPresenters()) { - if (presenter instanceof DvrItemPresenter) { - ((DvrItemPresenter) presenter).unbindAllViewHolders(); - } - } - super.onDestroy(); - } - - @Override - public void onDvrScheduleLoadFinished() { - List scheduledRecordings = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings)); - List seriesRecordings = mDvrDataManager.getSeriesRecordings(); - onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings)); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } - mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); - } - - @Override - public void onRecordedProgramLoadFinished() { - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, true); - } - updateRows(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } - mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramAdded(recordedProgram, true); - } - postUpdateRows(); - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramChanged(recordedProgram); - } - postUpdateRows(); - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramRemoved(recordedProgram); - } - postUpdateRows(); - } - - // No need to call updateRows() during ScheduledRecordings' change because - // the row for ScheduledRecordings is always displayed. - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - if (needToShowScheduledRecording(scheduleRecording)) { - mScheduleAdapter.add(scheduleRecording); - } - } - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - mScheduleAdapter.remove(scheduleRecording); - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduleRecording : scheduledRecordings) { - if (needToShowScheduledRecording(scheduleRecording)) { - mScheduleAdapter.change(scheduleRecording); - } else { - mScheduleAdapter.removeWithId(scheduleRecording); - } - } - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings)); - postUpdateRows(); - } - - // Workaround of b/29108300 - @Override - public void showTitle(int flags) { - flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE; - super.showTitle(flags); - } - - private void setupUiElements() { - setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); - setHeadersState(HEADERS_ENABLED); - setHeadersTransitionOnBackEnabled(false); - setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); - } - - private void setupAdapters() { - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); - mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new SeriesAdapter(); - for (int i = 0; i < mGenreAdapters.length; i++) { - mGenreAdapters[i] = new RecordedProgramAdapter(); - } - // Schedule Recordings. - List 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 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 void handleRecordedProgramAdded(RecordedProgram recordedProgram, - boolean updateSeriesRecording) { - mRecentAdapter.add(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - SeriesRecording seriesRecording = null; - if (seriesId != null) { - seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) < 0) { - mSeriesId2LatestProgram.put(seriesId, recordedProgram); - if (updateSeriesRecording && seriesRecording != null) { - onSeriesRecordingChanged(seriesRecording); - } - } - } - if (seriesRecording == null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { - adapter.add(recordedProgram); - } - } - } - - private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) { - mRecentAdapter.remove(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - if (seriesId != null) { - SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = - mSeriesId2LatestProgram.get(recordedProgram.getSeriesId()); - if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) { - if (seriesRecording != null) { - updateLatestRecordedProgram(seriesRecording); - onSeriesRecordingChanged(seriesRecording); - } - } - } - for (RecordedProgramAdapter adapter - : getGenreAdapters(recordedProgram.getCanonicalGenres())) { - adapter.remove(recordedProgram); - } - } - - private void handleRecordedProgramChanged(RecordedProgram recordedProgram) { - mRecentAdapter.change(recordedProgram); - String seriesId = recordedProgram.getSeriesId(); - SeriesRecording seriesRecording = null; - if (seriesId != null) { - seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); - RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); - if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .compare(latestProgram, recordedProgram) <= 0) { - mSeriesId2LatestProgram.put(seriesId, recordedProgram); - if (seriesRecording != null) { - onSeriesRecordingChanged(seriesRecording); - } - } else if (latestProgram.getId() == recordedProgram.getId()) { - if (seriesRecording != null) { - updateLatestRecordedProgram(seriesRecording); - onSeriesRecordingChanged(seriesRecording); - } - } - } - if (seriesRecording == null) { - updateGenreAdapters(getGenreAdapters( - recordedProgram.getCanonicalGenres()), recordedProgram); - } else { - updateGenreAdapters(new ArrayList<>(), recordedProgram); - } - } - - private void handleSeriesRecordingsAdded(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.add(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.add(seriesRecording); - } - } - } - } - - private void handleSeriesRecordingsRemoved(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.remove(seriesRecording); - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.remove(seriesRecording); - } - } - } - - private void handleSeriesRecordingsChanged(List seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - mSeriesAdapter.change(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - updateGenreAdapters(getGenreAdapters( - seriesRecording.getCanonicalGenreIds()), seriesRecording); - } else { - // Remove series recording from all genre rows if it has no recorded program - updateGenreAdapters(new ArrayList<>(), seriesRecording); - } - } - } - - private List getGenreAdapters(String[] genres) { - List result = new ArrayList<>(); - if (genres == null || genres.length == 0) { - result.add(mGenreAdapters[mGenreAdapters.length - 1]); - } else { - for (String genre : genres) { - int genreId = GenreItems.getId(genre); - if(genreId >= mGenreAdapters.length) { - Log.d(TAG, "Wrong Genre ID: " + genreId); - } else { - result.add(mGenreAdapters[genreId]); - } - } - } - return result; - } - - private List getGenreAdapters(int[] genreIds) { - List result = new ArrayList<>(); - if (genreIds == null || genreIds.length == 0) { - result.add(mGenreAdapters[mGenreAdapters.length - 1]); - } else { - for (int genreId : genreIds) { - if(genreId >= mGenreAdapters.length) { - Log.d(TAG, "Wrong Genre ID: " + genreId); - } else { - result.add(mGenreAdapters[genreId]); - } - } - } - return result; - } - - private void updateGenreAdapters(List adapters, Object r) { - for (RecordedProgramAdapter adapter : mGenreAdapters) { - if (adapters.contains(adapter)) { - adapter.change(r); - } else { - adapter.remove(r); - } - } - } - - private void postUpdateRows() { - mHandler.removeCallbacks(mUpdateRowsRunnable); - mHandler.post(mUpdateRowsRunnable); - } - - private void updateRows() { - int visibleRowsCount = 1; // Schedule's Row will never be empty - if (mRecentAdapter.isEmpty()) { - mRowsAdapter.remove(mRecentRow); - } else { - if (mRowsAdapter.indexOf(mRecentRow) < 0) { - mRowsAdapter.add(0, mRecentRow); - } - visibleRowsCount++; - } - if (mSeriesAdapter.isEmpty()) { - mRowsAdapter.remove(mSeriesRow); - } else { - if (mRowsAdapter.indexOf(mSeriesRow) < 0) { - mRowsAdapter.add(visibleRowsCount, mSeriesRow); - } - visibleRowsCount++; - } - for (int i = 0; i < mGenreAdapters.length; i++) { - RecordedProgramAdapter adapter = mGenreAdapters[i]; - if (adapter != null) { - if (adapter.isEmpty()) { - mRowsAdapter.remove(mGenreRows[i]); - } else { - if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) { - mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter); - mRowsAdapter.add(visibleRowsCount, mGenreRows[i]); - } - visibleRowsCount++; - } - } - } - } - - private boolean needToShowScheduledRecording(ScheduledRecording recording) { - int state = recording.getState(); - return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; - } - - private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { - RecordedProgram latestProgram = null; - for (RecordedProgram program : - mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { - if (latestProgram == null || RecordedProgram - .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { - latestProgram = program; - } - } - mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram); - } - - private class ScheduleAdapter extends SortedArrayAdapter { - ScheduleAdapter(int maxItemCount) { - super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount); - } - - @Override - public long getId(Object item) { - if (item instanceof ScheduledRecording) { - return ((ScheduledRecording) item).getId(); - } else { - return -1; - } - } - } - - private class SeriesAdapter extends SortedArrayAdapter { - SeriesAdapter() { - super(mPresenterSelector, new Comparator() { - @Override - public int compare(SeriesRecording lhs, SeriesRecording rhs) { - if (lhs.isStopped() && !rhs.isStopped()) { - return 1; - } else if (!lhs.isStopped() && rhs.isStopped()) { - return -1; - } - return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); - } - }); - } - - @Override - public long getId(SeriesRecording item) { - return item.getId(); - } - } - - private class RecordedProgramAdapter extends SortedArrayAdapter { - RecordedProgramAdapter() { - this(Integer.MAX_VALUE); - } - - RecordedProgramAdapter(int maxItemCount) { - super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount); - } - - @Override - public long getId(Object item) { - if (item instanceof SeriesRecording) { - return ((SeriesRecording) item).getId(); - } else if (item instanceof RecordedProgram) { - return ((RecordedProgram) item).getId(); - } else { - return -1; - } - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/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/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java deleted file mode 100644 index 806c775c..00000000 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ /dev/null @@ -1,98 +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.v17.leanback.app.DetailsFragment; - -import com.android.tv.R; -import com.android.tv.TvApplication; - -/** - * Activity to show details view in DVR. - */ -public class DvrDetailsActivity extends Activity { - /** - * Name of record id added to the Intent. - */ - public static final String RECORDING_ID = "record_id"; - - /** - * Name of flag added to the Intent to determine if details view should hide "View schedule" - * button. - */ - public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; - - /** - * Name of details view's type added to the intent. - */ - public static final String DETAILS_VIEW_TYPE = "details_view_type"; - - /** - * Name of shared element between activities. - */ - public static final String SHARED_ELEMENT_NAME = "shared_element"; - - /** - * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. - */ - public static final int CURRENT_RECORDING_VIEW = 1; - - /** - * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. - */ - public static final int SCHEDULED_RECORDING_VIEW = 2; - - /** - * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. - */ - public static final int RECORDED_PROGRAM_VIEW = 3; - - /** - * SERIES_RECORDING_VIEW refers to series recording in DVR. - */ - public static final int SERIES_RECORDING_VIEW = 4; - - @Override - public void onCreate(Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_dvr_details); - long recordId = getIntent().getLongExtra(RECORDING_ID, -1); - int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); - boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); - if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(RECORDING_ID, recordId); - DetailsFragment detailsFragment = null; - if (detailsViewType == CURRENT_RECORDING_VIEW) { - detailsFragment = new CurrentRecordingDetailsFragment(); - } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { - args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); - detailsFragment = new ScheduledRecordingDetailsFragment(); - } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { - detailsFragment = new RecordedProgramDetailsFragment(); - } else if (detailsViewType == SERIES_RECORDING_VIEW) { - detailsFragment = new SeriesRecordingDetailsFragment(); - } - detailsFragment.setArguments(args); - getFragmentManager().beginTransaction() - .replace(R.id.dvr_details_view_frame, detailsFragment).commit(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java deleted file mode 100644 index 21f9c4b4..00000000 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ /dev/null @@ -1,344 +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.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.media.tv.TvContentRating; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v17.leanback.app.DetailsFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.DetailsOverviewRow; -import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.support.v17.leanback.widget.VerticalGridView; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.widget.Toast; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.ImageLoader; -import com.android.tv.util.ToastUtils; -import com.android.tv.util.Utils; - -import java.io.File; - -abstract class DvrDetailsFragment extends DetailsFragment { - private static final int LOAD_LOGO_IMAGE = 1; - private static final int LOAD_BACKGROUND_IMAGE = 2; - - protected DetailsViewBackgroundHelper mBackgroundHelper; - private ArrayObjectAdapter mRowsAdapter; - private DetailsOverviewRow mDetailsOverview; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (!onLoadRecordingDetails(getArguments())) { - getActivity().finish(); - return; - } - mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); - setupAdapter(); - onCreateInternal(); - } - - @Override - public void onStart() { - super.onStart(); - // TODO: remove the workaround of b/30401180. - VerticalGridView container = (VerticalGridView) getActivity() - .findViewById(R.id.container_list); - // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. - container.setItemAlignmentOffset(0); - container.setWindowAlignmentOffset( - getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); - } - - private void setupAdapter() { - DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( - new DetailsContentPresenter(getActivity())); - rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, - null)); - rowPresenter.setSharedElementEnterTransition(getActivity(), - DvrDetailsActivity.SHARED_ELEMENT_NAME); - rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); - mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); - setAdapter(mRowsAdapter); - } - - /** - * Returns details views' rows adapter. - */ - protected ArrayObjectAdapter getRowsAdapter() { - return mRowsAdapter; - } - - /** - * Sets details overview. - */ - protected void setDetailsOverviewRow(DetailsContent detailsContent) { - mDetailsOverview = new DetailsOverviewRow(detailsContent); - mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); - mRowsAdapter.add(mDetailsOverview); - onLoadLogoAndBackgroundImages(detailsContent); - } - - /** - * Creates and returns presenter selector will be used by rows adaptor. - */ - protected PresenterSelector onCreatePresenterSelector( - DetailsOverviewRowPresenter rowPresenter) { - ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - return presenterSelector; - } - - /** - * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish - * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not - * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to - * do after the super class did onCreate, it should override this method and put the codes here. - */ - protected void onCreateInternal() { } - - /** - * Updates actions of details overview. - */ - protected void updateActions() { - mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); - } - - /** - * Loads recording details according to the arguments the fragment got. - * - * @return false if cannot find valid recordings, else return true. If the return value - * is false, the detail activity and fragment will be ended. - */ - abstract boolean onLoadRecordingDetails(Bundle args); - - /** - * Creates actions users can interact with and their adaptor for this fragment. - */ - abstract SparseArrayObjectAdapter onCreateActionsAdapter(); - - /** - * Creates actions listeners to implement the behavior of the fragment after users click some - * action buttons. - */ - abstract OnActionClickedListener onCreateOnActionClickedListener(); - - /** - * Returns program title with episode number. If the program is null, returns channel name. - */ - protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { - String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); - SpannableString title = titleWithEpisodeNumber == null ? null - : new SpannableString(titleWithEpisodeNumber); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - return title; - } - - /** - * Loads logo and background images for detail fragments. - */ - protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { - Drawable logoDrawable = null; - Drawable backgroundDrawable = null; - if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { - logoDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); - mDetailsOverview.setImageDrawable(logoDrawable); - } - if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { - backgroundDrawable = getContext().getResources() - .getDrawable(R.drawable.dvr_default_poster, null); - mBackgroundHelper.setBackground(backgroundDrawable); - } - if (logoDrawable != null && backgroundDrawable != null) { - return; - } - if (logoDrawable == null && backgroundDrawable == null - && detailsContent.getLogoImageUri().equals( - detailsContent.getBackgroundImageUri())) { - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, - getContext())); - return; - } - if (logoDrawable == null) { - int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); - int imageHeight = getResources() - .getDimensionPixelSize(R.dimen.dvr_details_poster_height); - ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), - imageWidth, imageHeight, - new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); - } - if (backgroundDrawable == null) { - ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), - new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); - } - } - - protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { - if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && - !isDataUriAccessible(recordedProgram.getDataUri())) { - // Since cleaning RecordedProgram from forgotten storage will take some time, - // ignore playback until cleaning is finished. - ToastUtils.show(getContext(), - getContext().getResources().getString(R.string.dvr_toast_recording_deleted), - Toast.LENGTH_SHORT); - return; - } - ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) - .getTvInputManagerHelper().getParentalControlSettings(); - if (!parental.isParentalControlsEnabled()) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - return; - } - ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); - Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); - if (channel != null && channel.isLocked()) { - checkPinToPlay(recordedProgram, seekTimeMs); - return; - } - String ratingString = recordedProgram.getContentRating(); - if (TextUtils.isEmpty(ratingString)) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - return; - } - String[] ratingList = ratingString.split(","); - TvContentRating[] programRatings = new TvContentRating[ratingList.length]; - for (int i = 0; i < ratingList.length; i++) { - programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); - } - TvContentRating blockRatings = parental.getBlockedRating(programRatings); - if (blockRatings != null) { - checkPinToPlay(recordedProgram, seekTimeMs); - } else { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - } - } - - private boolean isDataUriAccessible(Uri dataUri) { - if (dataUri == null || dataUri.getPath() == null) { - return false; - } - try { - File recordedProgramPath = new File(dataUri.getPath()); - if (recordedProgramPath.exists()) { - return true; - } - } catch (SecurityException e) { - } - return false; - } - - private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - launchPlaybackActivity(recordedProgram, seekTimeMs, true); - } - } - }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - - private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, - boolean pinChecked) { - Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); - if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); - } - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); - getActivity().startActivity(intent); - } - - private static class MyImageLoaderCallback extends - ImageLoader.ImageLoaderCallback { - private final Context mContext; - private final int mLoadType; - - public MyImageLoaderCallback(DvrDetailsFragment fragment, - int loadType, Context context) { - super(fragment); - mLoadType = loadType; - mContext = context; - } - - @Override - public void onBitmapLoaded(DvrDetailsFragment fragment, - @Nullable Bitmap bitmap) { - Drawable drawable; - int loadType = mLoadType; - if (bitmap == null) { - Resources res = mContext.getResources(); - drawable = res.getDrawable(R.drawable.dvr_default_poster, null); - if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { - loadType &= ~LOAD_BACKGROUND_IMAGE; - fragment.mBackgroundHelper.setBackgroundColor( - res.getColor(R.color.dvr_detail_default_background)); - fragment.mBackgroundHelper.setScrim( - res.getColor(R.color.dvr_detail_default_background_scrim)); - } - } else { - drawable = new BitmapDrawable(mContext.getResources(), bitmap); - } - if (!fragment.isDetached()) { - if ((loadType & LOAD_LOGO_IMAGE) != 0) { - fragment.mDetailsOverview.setImageDrawable(drawable); - } - if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { - fragment.mBackgroundHelper.setBackground(drawable); - } - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/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 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 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; @@ -165,6 +166,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 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 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/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java deleted file mode 100644 index 339e5d2f..00000000 --- a/src/com/android/tv/dvr/ui/DvrItemPresenter.java +++ /dev/null @@ -1,80 +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.support.annotation.CallSuper; -import android.support.v17.leanback.widget.Presenter; -import android.view.View; -import android.view.View.OnClickListener; - -import com.android.tv.dvr.DvrUiHelper; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -/** - * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, - * {@link RecordedProgram}, and {@link SeriesRecording}. - */ -public abstract class DvrItemPresenter extends Presenter { - private final Set mBoundViewHolders = new HashSet<>(); - private final OnClickListener mOnClickListener = onCreateOnClickListener(); - - @Override - @CallSuper - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - viewHolder.view.setTag(o); - viewHolder.view.setOnClickListener(mOnClickListener); - mBoundViewHolders.add(viewHolder); - } - - @Override - @CallSuper - public void onUnbindViewHolder(ViewHolder viewHolder) { - mBoundViewHolders.remove(viewHolder); - } - - /** - * Unbinds all bound view holders. - */ - public void unbindAllViewHolders() { - // When browse fragments are destroyed, RecyclerView would not call presenters' - // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. - for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { - onUnbindViewHolder(viewHolder); - } - } - - /** - * Creates {@link OnClickListener} for DVR library's card views. - */ - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View view) { - if (view instanceof RecordingCardView) { - RecordingCardView v = (RecordingCardView) view; - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), - v.getTag(), v.getImageView(), false); - } - } - }; - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/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 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/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java deleted file mode 100644 index 8c4c856c..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ /dev/null @@ -1,82 +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.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.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.util.Utils; - -/** - * This class is used to generate Views and bind Objects for related recordings in DVR playback. - */ -public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { - private static final String TAG = "DvrPlaybackCardPresenter"; - private static final boolean DEBUG = false; - - private final int mRelatedRecordingCardWidth; - private final int mRelatedRecordingCardHeight; - - DvrPlaybackCardPresenter(Context context) { - super(context); - mRelatedRecordingCardWidth = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); - mRelatedRecordingCardHeight = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Resources res = parent.getResources(); - RecordingCardView view = new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight); - return new ViewHolder(view); - } - - @Override - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - long programId = ((RecordedProgram) v.getTag()).getId(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); - } - }; - } - - @Override - protected String getDescription(RecordedProgram program) { - String description = program.getDescription(); - if (TextUtils.isEmpty(description)) { - description = - getContext().getResources().getString(R.string.dvr_msg_no_program_description); - } - return description; - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java deleted file mode 100644 index 0bc4ecb1..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ /dev/null @@ -1,313 +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.graphics.drawable.Drawable; -import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaController.TransportControls; -import android.media.session.PlaybackState; -import android.support.v17.leanback.app.PlaybackControlGlue; -import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.RowPresenter; -import android.text.TextUtils; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; - -import com.android.tv.R; -import com.android.tv.util.TimeShiftUtils; - -/** - * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and - * send command to the media controller. It also helps to update playback states displayed in the - * fragment according to information the media session provides. - */ -public class DvrPlaybackControlHelper extends PlaybackControlGlue { - private static final String TAG = "DvrPlaybackControlHelper"; - private static final boolean DEBUG = false; - - /** - * Indicates the ID of the media under playback is unknown. - */ - public static int UNKNOWN_MEDIA_ID = -1; - - private int mPlaybackState = PlaybackState.STATE_NONE; - private int mPlaybackSpeedLevel; - private int mPlaybackSpeedId; - private boolean mReadyToControl; - - private final MediaController mMediaController; - private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); - private final TransportControls mTransportControls; - private final int mExtraPaddingTopForNoDescription; - - public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { - super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); - mMediaController = activity.getMediaController(); - mMediaController.registerCallback(mMediaControllerCallback); - mTransportControls = mMediaController.getTransportControls(); - mExtraPaddingTopForNoDescription = activity.getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); - } - - @Override - public PlaybackControlsRowPresenter createControlsRowAndPresenter() { - PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); - setControlsRow(controlsRow); - AbstractDetailsDescriptionPresenter detailsPresenter = - new AbstractDetailsDescriptionPresenter() { - @Override - protected void onBindDescription( - AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { - PlaybackControlGlue glue = (PlaybackControlGlue) object; - if (glue.hasValidMedia()) { - viewHolder.getTitle().setText(glue.getMediaTitle()); - viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); - } else { - viewHolder.getTitle().setText(""); - viewHolder.getSubtitle().setText(""); - } - if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { - viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), - mExtraPaddingTopForNoDescription, - viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); - } - } - }; - PlaybackControlsRowPresenter presenter = - new PlaybackControlsRowPresenter(detailsPresenter) { - @Override - protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { - super.onBindRowViewHolder(vh, item); - vh.setOnKeyListener(DvrPlaybackControlHelper.this); - } - - @Override - protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { - super.onUnbindRowViewHolder(vh); - vh.setOnKeyListener(null); - } - }; - presenter.setProgressColor(getContext().getResources() - .getColor(R.color.play_controls_progress_bar_watched)); - presenter.setBackgroundColor(getContext().getResources() - .getColor(R.color.play_controls_body_background_enabled)); - presenter.setOnActionClickedListener(new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (mReadyToControl) { - DvrPlaybackControlHelper.super.onActionClicked(action); - } - } - }); - return presenter; - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (mReadyToControl) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN - && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING - || mPlaybackState == PlaybackState.STATE_REWINDING)) { - // Workaround of b/31489271. Clicks play/pause button first to reset play controls - // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. - onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); - } - return super.onKey(v, keyCode, event); - } - return false; - } - - @Override - public boolean hasValidMedia() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - return playbackState != null; - } - - @Override - public boolean isMediaPlaying() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return false; - } - int state = playbackState.getState(); - return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING - && state != PlaybackState.STATE_PAUSED; - } - - /** - * Returns the ID of the media under playback. - */ - public long getMediaId() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? UNKNOWN_MEDIA_ID - : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID); - } - - @Override - public CharSequence getMediaTitle() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" - : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - } - - @Override - public CharSequence getMediaSubtitle() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? "" - : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); - } - - @Override - public int getMediaDuration() { - MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? 0 - : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); - } - - @Override - public Drawable getMediaArt() { - // Do not show the poster art on control row. - return null; - } - - @Override - public long getSupportedActions() { - return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; - } - - @Override - public int getCurrentSpeedId() { - return mPlaybackSpeedId; - } - - @Override - public int getCurrentPosition() { - PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return 0; - } - return (int) playbackState.getPosition(); - } - - /** - * Unregister media controller's callback. - */ - public void unregisterCallback() { - mMediaController.unregisterCallback(mMediaControllerCallback); - } - - @Override - protected void startPlayback(int speedId) { - if (getCurrentSpeedId() == speedId) { - return; - } - if (speedId == PLAYBACK_SPEED_NORMAL) { - mTransportControls.play(); - } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { - mTransportControls.rewind(); - } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ - mTransportControls.fastForward(); - } - } - - @Override - protected void pausePlayback() { - mTransportControls.pause(); - } - - @Override - protected void skipToNext() { - // Do nothing. - } - - @Override - protected void skipToPrevious() { - // Do nothing. - } - - @Override - protected void onRowChanged(PlaybackControlsRow row) { - // Do nothing. - } - - private void onStateChanged(int state, long positionMs, int speedLevel) { - if (DEBUG) Log.d(TAG, "onStateChanged"); - getControlsRow().setCurrentTime((int) positionMs); - if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { - // Only position is changed, no need to update controls row - return; - } - // NOTICE: The below two variables should only be used in this method. - // The only usage of them is to confirm if the state is changed or not. - mPlaybackState = state; - mPlaybackSpeedLevel = speedLevel; - switch (state) { - case PlaybackState.STATE_PLAYING: - mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; - setFadingEnabled(true); - mReadyToControl = true; - break; - case PlaybackState.STATE_PAUSED: - mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; - setFadingEnabled(true); - mReadyToControl = true; - break; - case PlaybackState.STATE_FAST_FORWARDING: - mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; - setFadingEnabled(false); - mReadyToControl = true; - break; - case PlaybackState.STATE_REWINDING: - mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; - setFadingEnabled(false); - mReadyToControl = true; - break; - case PlaybackState.STATE_CONNECTING: - setFadingEnabled(false); - mReadyToControl = false; - break; - case PlaybackState.STATE_NONE: - mReadyToControl = false; - break; - default: - setFadingEnabled(true); - break; - } - onStateChanged(); - } - - private class MediaControllerCallback extends MediaController.Callback { - @Override - public void onPlaybackStateChanged(PlaybackState state) { - if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); - onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); - } - - @Override - public void onMetadataChanged(MediaMetadata metadata) { - DvrPlaybackControlHelper.this.onMetadataChanged(); - ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java deleted file mode 100644 index 51ec93b8..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ /dev/null @@ -1,304 +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.Context; -import android.content.Intent; -import android.graphics.Point; -import android.hardware.display.DisplayManager; -import android.media.tv.TvContentRating; -import android.os.Bundle; -import android.media.session.PlaybackState; -import android.media.tv.TvInputManager; -import android.media.tv.TvView; -import android.support.v17.leanback.app.PlaybackOverlayFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.SinglePresenterSelector; -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.parental.ContentRatingsManager; -import com.android.tv.util.Utils; - -public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { - // TODO: Handles audio focus. Deals with block and ratings. - private static final String TAG = "DvrPlaybackOverlayFragment"; - private static final boolean DEBUG = false; - - private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; - private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; - - // mProgram is only used to store program from intent. Don't use it elsewhere. - private RecordedProgram mProgram; - private DvrPlaybackMediaSessionHelper mMediaSessionHelper; - private DvrPlaybackControlHelper mPlaybackControlHelper; - private ArrayObjectAdapter mRowsAdapter; - private SortedArrayAdapter mRelatedRecordingsRowAdapter; - private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; - private DvrDataManager mDvrDataManager; - private ContentRatingsManager mContentRatingsManager; - private TvView mTvView; - private View mBlockScreenView; - private ListRow mRelatedRecordingsRow; - private int mExtraPaddingNoRelatedRow; - private int mWindowWidth; - private int mWindowHeight; - private float mAppliedAspectRatio; - private float mWindowAspectRatio; - private boolean mPinChecked; - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - mExtraPaddingNoRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top); - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getContentRatingsManager(); - mProgram = getProgramFromIntent(getActivity().getIntent()); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - getActivity().finish(); - return; - } - Point size = new Point(); - ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) - .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); - mWindowWidth = size.x; - mWindowHeight = size.y; - mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; - setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); - setFadingEnabled(true); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); - mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); - mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); - setUpRows(); - preparePlayback(getActivity().getIntent()); - DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer(); - dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() { - @Override - public void onAspectRatioChanged(float videoAspectRatio) { - updateAspectRatio(videoAspectRatio); - } - }); - mPinChecked = getActivity().getIntent() - .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); - dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() { - @Override - public void onContentBlocked(TvContentRating rating) { - if (mPinChecked) { - mTvView.unblockContent(rating); - return; - } - mBlockScreenView.setVisibility(View.VISIBLE); - getActivity().getMediaController().getTransportControls().pause(); - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - mPinChecked = true; - mTvView.unblockContent(rating); - mBlockScreenView.setVisibility(View.GONE); - getActivity().getMediaController() - .getTransportControls().play(); - } - } - }, mContentRatingsManager.getDisplayNameForRating(rating)) - .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - }); - } - - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause"); - super.onPause(); - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING - || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { - getActivity().getMediaController().getTransportControls().pause(); - } - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { - getActivity().requestVisibleBehind(false); - } else { - getActivity().requestVisibleBehind(true); - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mPlaybackControlHelper.unregisterCallback(); - mMediaSessionHelper.release(); - mRelatedRecordingCardPresenter.unbindAllViewHolders(); - super.onDestroy(); - } - - /** - * Passes the intent to the fragment. - */ - public void onNewIntent(Intent intent) { - mProgram = getProgramFromIntent(intent); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - // Continue playing the original program - return; - } - preparePlayback(intent); - } - - /** - * Should be called when windows' size is changed in order to notify DVR player - * to update it's view width/height and position. - */ - public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { - mWindowWidth = windowWidth; - mWindowHeight = windowHeight; - mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; - updateAspectRatio(mAppliedAspectRatio); - } - - public RecordedProgram getNextEpisode(RecordedProgram program) { - int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); - if (position == mRelatedRecordingsRowAdapter.size()) { - return null; - } else { - return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); - } - } - - void onMediaControllerUpdated() { - mRowsAdapter.notifyArrayItemRangeChanged(0, 1); - } - - private void updateAspectRatio(float videoAspectRatio) { - if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { - // No need to change - return; - } - if (videoAspectRatio < mWindowAspectRatio) { - int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); - } else { - int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); - } - mAppliedAspectRatio = videoAspectRatio; - } - - private void preparePlayback(Intent intent) { - mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); - getActivity().getMediaController().getTransportControls().prepare(); - updateRelatedRecordingsRow(); - } - - private void updateRelatedRecordingsRow() { - boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); - mRelatedRecordingsRowAdapter.clear(); - long programId = mProgram.getId(); - String seriesId = mProgram.getSeriesId(); - if (!TextUtils.isEmpty(seriesId)) { - if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); - for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) { - if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) { - mRelatedRecordingsRowAdapter.add(program); - } - } - } - View view = getView(); - if (mRelatedRecordingsRowAdapter.size() == 0) { - mRowsAdapter.remove(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow, - view.getPaddingRight(), view.getPaddingBottom()); - } else if (wasEmpty){ - mRowsAdapter.add(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), 0, - view.getPaddingRight(), view.getPaddingBottom()); - } - } - - private void setUpRows() { - PlaybackControlsRowPresenter controlsRowPresenter = - mPlaybackControlHelper.createControlsRowAndPresenter(); - - ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); - selector.addClassPresenter(ListRow.class, new ListRowPresenter()); - - mRowsAdapter = new ArrayObjectAdapter(selector); - mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); - mRelatedRecordingsRow = getRelatedRecordingsRow(); - setAdapter(mRowsAdapter); - } - - private ListRow getRelatedRecordingsRow() { - mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); - mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); - HeaderItem header = new HeaderItem(0, - getActivity().getString(R.string.dvr_playback_related_recordings)); - return new ListRow(header, mRelatedRecordingsRowAdapter); - } - - private RecordedProgram getProgramFromIntent(Intent intent) { - long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); - return mDvrDataManager.getRecordedProgram(programId); - } - - private long getSeekTimeFromIntent(Intent intent) { - return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, - TvInputManager.TIME_SHIFT_INVALID_TIME); - } - - private class RelatedRecordingsAdapter extends SortedArrayAdapter { - RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { - super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); - } - - @Override - long getId(BaseProgram item) { - return item.getId(); - } - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java new file mode 100644 index 00000000..562898a3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.FragmentManager; +import android.content.Context; +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment for DVR series recording settings. + */ +public class DvrPrioritySettingsFragment extends GuidedStepFragment { + /** + * Name of series recording id starting the fragment. + * Type: Long + */ + public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id"; + + private static final int ONE_TIME_RECORDING_ID = 0; + // button action's IDs are negative. + private static final long ACTION_ID_SAVE = -100L; + + private final List mSeriesRecordings = new ArrayList<>(); + + private SeriesRecording mSelectedRecording; + private SeriesRecording mComeFromSeriesRecording; + private float mSelectedActionElevation; + private int mActionColor; + private int mSelectedActionColor; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSeriesRecordings.clear(); + mSeriesRecordings.add(new SeriesRecording.Builder() + .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) + .setPriority(Long.MAX_VALUE) + .setId(ONE_TIME_RECORDING_ID) + .build()); + DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + long comeFromSeriesRecordingId = + getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); + for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { + if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL + || series.getId() == comeFromSeriesRecordingId) { + mSeriesRecordings.add(series); + } + } + mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); + mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); + mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); + mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); + mSelectedActionColor = + getResources().getColor(R.color.dvr_guided_step_action_text_color_selected, null); + } + + @Override + public void onResume() { + super.onResume(); + setSelectedActionPosition(mComeFromSeriesRecording == null ? 1 + : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = mComeFromSeriesRecording == null ? null + : mComeFromSeriesRecording.getTitle(); + return new Guidance(getString(R.string.dvr_priority_title), + getString(R.string.dvr_priority_description), breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + int position = 0; + for (SeriesRecording seriesRecording : mSeriesRecordings) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(position++) + .title(seriesRecording.getTitle()) + .build()); + } + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SAVE) + .title(getString(R.string.dvr_priority_button_action_save)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_ID_SAVE) { + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + int size = mSeriesRecordings.size(); + for (int i = 1; i < size; ++i) { + long priority = DvrScheduleManager.suggestSeriesPriority(size - i); + SeriesRecording seriesRecording = mSeriesRecordings.get(i); + if (seriesRecording.getPriority() != priority) { + dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording) + .setPriority(priority).build()); + } + } + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.popBackStack(); + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.popBackStack(); + } else if (mSelectedRecording == null) { + mSelectedRecording = mSeriesRecordings.get((int) actionId); + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + } else { + mSelectedRecording = null; + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + super.onGuidedActionFocused(action); + if (mSelectedRecording == null) { + return; + } + if (action.getId() < 0) { + mSelectedRecording = null; + for (int i = 0; i < mSeriesRecordings.size(); ++i) { + updateItem(i); + } + return; + } + int position = (int) action.getId(); + int previousPosition = mSeriesRecordings.indexOf(mSelectedRecording); + mSeriesRecordings.remove(mSelectedRecording); + mSeriesRecordings.add(position, mSelectedRecording); + updateItem(previousPosition); + updateItem(position); + notifyActionChanged(previousPosition); + notifyActionChanged(position); + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new DvrGuidedActionsStylist(false) { + @Override + public void onBindViewHolder(ViewHolder vh, GuidedAction action) { + super.onBindViewHolder(vh, action); + updateItem(vh.itemView, (int) action.getId()); + } + + @Override + public int onProvideItemLayoutId() { + return R.layout.priority_settings_action_item; + } + }; + } + + private void updateItem(int position) { + View itemView = getActionItemView(position); + if (itemView == null) { + return; + } + updateItem(itemView, position); + } + + private void updateItem(View itemView, int position) { + GuidedAction action = getActions().get(position); + action.setTitle(mSeriesRecordings.get(position).getTitle()); + boolean selected = mSelectedRecording != null + && mSeriesRecordings.indexOf(mSelectedRecording) == position; + TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title); + ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image); + if (position == 0) { + // one-time recording + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setVisibility(View.GONE); + itemView.setFocusable(false); + itemView.setElevation(0); + // strings.xml tag doesn't work. + titleView.setTypeface(titleView.getTypeface(), Typeface.ITALIC); + } else if (mSelectedRecording == null) { + titleView.setTextColor(mActionColor); + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setImageResource(R.drawable.ic_draggable_white); + imageView.setVisibility(View.VISIBLE); + itemView.setFocusable(true); + itemView.setElevation(0); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } else if (selected) { + titleView.setTextColor(mSelectedActionColor); + itemView.setBackgroundResource(R.drawable.priority_settings_action_item_selected); + imageView.setImageResource(R.drawable.ic_dragging_grey); + imageView.setVisibility(View.VISIBLE); + itemView.setFocusable(true); + itemView.setElevation(mSelectedActionElevation); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } else { + titleView.setTextColor(mActionColor); + itemView.setBackgroundResource(R.drawable.setup_selector_background); + imageView.setVisibility(View.INVISIBLE); + itemView.setFocusable(true); + itemView.setElevation(0); + titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); + } + } +} \ 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/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java deleted file mode 100644 index f6e6ac26..00000000 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.os.Bundle; -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 java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Activity to show the list of recording schedules. - */ -public class DvrSchedulesActivity extends Activity { - /** - * The key for the type of the schedules which will be listed in the list. The type of the value - * should be {@link ScheduleListType}. - */ - public static final String KEY_SCHEDULES_TYPE = "schedules_type"; - - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE}) - public @interface ScheduleListType {} - /** - * A type which means the activity will display the full scheduled recordings. - */ - public static final int TYPE_FULL_SCHEDULE = 0; - /** - * A type which means the activity will display a scheduled recording list of a series - * recording. - */ - public static final int TYPE_SERIES_SCHEDULE = 1; - - @Override - public void onCreate(final Bundle savedInstanceState) { - TvApplication.setCurrentRunningProcess(this, true); - // Pass null to prevent automatically re-creating fragments - super.onCreate(null); - setContentView(R.layout.activity_dvr_schedules); - int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); - if (scheduleType == TYPE_FULL_SCHEDULE) { - DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_schedules_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); - // To get programs faster, hold the update of the series schedules. - SeriesRecordingScheduler.getInstance(this).pauseUpdate(); - new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { - @Override - protected void onPostExecute(List programs) { - SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); - dialog.dismiss(); - Bundle args = getIntent().getExtras(); - args.putParcelableArrayList(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); - DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true) - .execute(); - } else { - finish(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index 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/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java new file mode 100644 index 00000000..8bf8560f --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.text.TextUtils; +import android.view.ViewGroup.LayoutParams; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.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; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Fragment for DVR series recording settings. + */ +public class 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, + // negative values are used by other actions to prevent duplicated IDs. + private static final long ACTION_ID_SELECT_WATCHED = -110; + private static final long ACTION_ID_SELECT_ALL = -111; + private static final long ACTION_ID_DELETE = -112; + + private DvrDataManager mDvrDataManager; + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private List mRecordings; + private final Set mWatchedRecordings = new HashSet<>(); + private boolean mAllSelected; + private long mSeriesRecordingId; + private int mOneLineActionHeight; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mSeriesRecordingId = getArguments() + .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); + SoftPreconditions.checkArgument(mSeriesRecordingId != -1); + mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrWatchedPositionManager = + TvApplication.getSingletons(context).getDvrWatchedPositionManager(); + mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); + mOneLineActionHeight = getResources().getDimensionPixelSize( + R.dimen.dvr_settings_one_line_action_container_height); + if (mRecordings.isEmpty()) { + Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings), + Toast.LENGTH_LONG).show(); + finishGuidedStepFragments(); + return; + } + Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = null; + SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (series != null) { + breadcrumb = series.getTitle(); + } + return new Guidance(getString(R.string.dvr_series_deletion_title), + getString(R.string.dvr_series_deletion_description), breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_WATCHED) + .title(getString(R.string.dvr_series_select_watched)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_SELECT_ALL) + .title(getString(R.string.dvr_series_select_all)) + .build()); + actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); + for (RecordedProgram recording : mRecordings) { + long watchedPositionMs = + mDvrWatchedPositionManager.getWatchedPosition(recording.getId()); + String title = recording.getEpisodeDisplayTitle(getContext()); + if (TextUtils.isEmpty(title)) { + title = TextUtils.isEmpty(recording.getTitle()) ? + getString(R.string.channel_banner_no_title) : recording.getTitle(); + } + String description; + if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + description = getWatchedString(watchedPositionMs, recording.getDurationMillis()); + mWatchedRecordings.add(recording.getId()); + } else { + description = getString(R.string.dvr_series_never_watched); + } + actions.add(new GuidedAction.Builder(getActivity()) + .id(recording.getId()) + .title(title) + .description(description) + .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) + .build()); + } + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_DELETE) + .title(getString(R.string.dvr_detail_delete)) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_ID_DELETE) { + List idsToDelete = new ArrayList<>(); + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID + && guidedAction.isChecked()) { + idsToDelete.add(guidedAction.getId()); + } + } + if (!idsToDelete.isEmpty()) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedPrograms(idsToDelete); + } + Toast.makeText(getContext(), getResources().getQuantityString( + R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), + mRecordings.size()), Toast.LENGTH_LONG).show(); + finishGuidedStepFragments(); + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + finishGuidedStepFragments(); + } else if (actionId == ACTION_ID_SELECT_WATCHED) { + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + long recordingId = guidedAction.getId(); + if (mWatchedRecordings.contains(recordingId)) { + guidedAction.setChecked(true); + } else { + guidedAction.setChecked(false); + } + notifyActionChanged(findActionPositionById(recordingId)); + } + } + mAllSelected = updateSelectAllState(); + } else if (actionId == ACTION_ID_SELECT_ALL) { + mAllSelected = !mAllSelected; + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + guidedAction.setChecked(mAllSelected); + notifyActionChanged(findActionPositionById(guidedAction.getId())); + } + } + updateSelectAllState(action, mAllSelected); + } else { + mAllSelected = updateSelectAllState(); + } + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + @Override + public GuidedActionsStylist onCreateActionsStylist() { + return new GuidedActionsStylistWithDivider() { + @Override + public void onBindViewHolder(ViewHolder vh, GuidedAction action) { + super.onBindViewHolder(vh, action); + if (action.getId() == ACTION_DIVIDER) { + return; + } + LayoutParams lp = vh.itemView.getLayoutParams(); + if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) { + lp.height = mOneLineActionHeight; + } else { + vh.itemView.setLayoutParams( + new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT)); + } + } + }; + } + + private String getWatchedString(long watchedPositionMs, long durationMs) { + if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { + return getResources().getString(R.string.dvr_series_watched_info_minutes, + Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), + Utils.getRoundOffMinsFromMs(durationMs)); + } else { + return getResources().getString(R.string.dvr_series_watched_info_seconds, + Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), + TimeUnit.MILLISECONDS.toSeconds(durationMs)); + } + } + + private boolean updateSelectAllState() { + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { + if (!guidedAction.isChecked()) { + if (mAllSelected) { + updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false); + } + return false; + } + } + } + if (!mAllSelected) { + updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true); + } + return true; + } + + private void updateSelectAllState(GuidedAction selectAll, boolean select) { + selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all) + : getString(R.string.dvr_series_select_all)); + notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL)); + } +} diff --git a/src/com/android/tv/dvr/ui/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 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) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); + BigArguments.reset(); mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + DvrScheduleManager dvrScheduleManager = + TvApplication.getSingletons(context).getDvrScheduleManager(); List 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/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java new file mode 100644 index 00000000..f28382da --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.FragmentManager; +import android.content.Context; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.util.LongSparseArray; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.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.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Fragment for DVR series recording settings. + */ +public class DvrSeriesSettingsFragment extends GuidedStepFragment + implements DvrDataManager.SeriesRecordingListener { + private static final String TAG = "SeriesSettingsFragment"; + private static final boolean DEBUG = false; + + private static final long ACTION_ID_PRIORITY = 10; + private static final long ACTION_ID_CHANNEL = 11; + + private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; + // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id + private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; + + private DvrDataManager mDvrDataManager; + private SeriesRecording mSeriesRecording; + private long mSeriesRecordingId; + @ChannelOption int mChannelOption; + private long mSelectedChannelId; + private int mBackStackCount; + private boolean mShowViewScheduleOptionInDialog; + private Program mCurrentProgram; + + private String mFragmentTitle; + private String mProrityActionTitle; + private String mProrityActionHighestText; + private String mProrityActionLowestText; + private String mChannelsActionTitle; + private String mChannelsActionAllText; + private LongSparseArray mId2Channel = new LongSparseArray<>(); + private List mChannels = new ArrayList<>(); + private List mPrograms; + + private GuidedAction mPriorityGuidedAction; + private GuidedAction mChannelsGuidedAction; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mBackStackCount = getFragmentManager().getBackStackEntryCount(); + mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); + mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (mSeriesRecording == null) { + getActivity().finish(); + return; + } + mShowViewScheduleOptionInDialog = getArguments().getBoolean( + DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); + mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); + mDvrDataManager.addSeriesRecordingListener(this); + mPrograms = (List) BigArguments.getArgument( + DvrSeriesSettingsActivity.PROGRAM_LIST); + BigArguments.reset(); + if (mPrograms == null) { + getActivity().finish(); + return; + } + Set 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); + } + } + } + mChannelOption = mSeriesRecording.getChannelOption(); + mSelectedChannelId = Channel.INVALID_ID; + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { + Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); + if (channel != null) { + mSelectedChannelId = channel.getId(); + } else { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + } + } + 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); + mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest); + mChannelsActionTitle = getString(R.string.dvr_series_settings_channels); + mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all); + } + + @Override + public void 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); + } + + @Override + public void onDestroy() { + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); + } + super.onDestroy(); + } + + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String breadcrumb = mSeriesRecording.getTitle(); + String title = mFragmentTitle; + return new Guidance(title, null, breadcrumb, null); + } + + @Override + public void onCreateActions(List actions, Bundle savedInstanceState) { + mPriorityGuidedAction = new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_PRIORITY) + .title(mProrityActionTitle) + .build(); + actions.add(mPriorityGuidedAction); + + mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_CHANNEL) + .title(mChannelsActionTitle) + .subActions(buildChannelSubAction()) + .build(); + actions.add(mChannelsGuidedAction); + updateChannelsGuidedAction(false); + } + + @Override + public void onCreateButtonActions(List actions, Bundle savedInstanceState) { + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + actions.add(new GuidedAction.Builder(getActivity()) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == GuidedAction.ACTION_ID_OK) { + if (mChannelOption != mSeriesRecording.getChannelOption() + || mSeriesRecording.isStopped() + || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mSeriesRecording.getChannelId() != mSelectedChannelId)) { + SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) + .setChannelOption(mChannelOption) + .setState(SeriesRecording.STATE_SERIES_NORMAL); + if (mSelectedChannelId != Channel.INVALID_ID) { + builder.setChannelId(mSelectedChannelId); + } + 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(); + } + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + finishGuidedStepFragments(); + } else if (actionId == ACTION_ID_PRIORITY) { + FragmentManager fragmentManager = getFragmentManager(); + DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); + Bundle args = new Bundle(); + args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + mSeriesRecording.getId()); + fragment.setArguments(args); + GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); + } + } + + @Override + public boolean onSubGuidedActionClicked(GuidedAction action) { + long actionId = action.getId(); + if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + mSelectedChannelId = Channel.INVALID_ID; + updateChannelsGuidedAction(true); + return true; + } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; + mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; + updateChannelsGuidedAction(true); + return true; + } + return false; + } + + @Override + public GuidedActionsStylist onCreateButtonActionsStylist() { + return new DvrGuidedActionsStylist(true); + } + + private void updateChannelsGuidedAction(boolean notifyActionChanged) { + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { + mChannelsGuidedAction.setDescription(mChannelsActionAllText); + } else if (mId2Channel.get(mSelectedChannelId) != null){ + mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) + .getDisplayText()); + } + if (notifyActionChanged) { + notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); + } + } + + private void updatePriorityGuidedAction() { + int totalSeriesCount = 0; + int priorityOrder = 0; + for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { + if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL + || seriesRecording.getId() == mSeriesRecording.getId()) { + ++totalSeriesCount; + } + if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL + && seriesRecording.getId() != mSeriesRecording.getId() + && seriesRecording.getPriority() > mSeriesRecording.getPriority()) { + ++priorityOrder; + } + } + if (priorityOrder == 0) { + mPriorityGuidedAction.setDescription(mProrityActionHighestText); + } else if (priorityOrder >= totalSeriesCount - 1) { + mPriorityGuidedAction.setDescription(mProrityActionLowestText); + } else { + mPriorityGuidedAction.setDescription(getString( + R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); + } + notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); + } + + private void updateSchedulesToSeries() { + List recordingCandidates = new ArrayList<>(); + Set 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())); + } + } + 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); + } + } + if (recordingCandidates.isEmpty()) { + return; + } + List programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) + .get(mSeriesRecordingId); + if (!programsToSchedule.isEmpty()) { + TvApplication.getSingletons(getContext()).getDvrManager() + .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); + } + } + + private List buildChannelSubAction() { + List channelSubActions = new ArrayList<>(); + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ALL) + .title(mChannelsActionAllText) + .build()); + for (Channel channel : mChannels) { + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) + .title(channel.getDisplayText()) + .build()); + } + return channelSubActions; + } + + private void showConfirmDialog() { + DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, + mShowViewScheduleOptionInDialog, mPrograms); + finishGuidedStepFragments(); + } + + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + 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(); + 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/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java new file mode 100644 index 00000000..507db6e7 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.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.widget.ImageView; +import android.widget.Toast; + +import com.android.tv.MainActivity; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.data.Program; +import com.android.tv.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.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. + */ +@MainThread +@TargetApi(Build.VERSION_CODES.N) +public class DvrUiHelper { + 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. + * + * @param recordingRequestRunnable if the storage status is OK to record or users choose to + * perform the operation anyway, this Runnable will run. + */ + 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(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(Activity activity, Channel channel) { + if (SoftPreconditions.checkNotNull(channel) == null) { + return; + } + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); + showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args); + } + + /** + * Shows the dialog which says that the new schedule conflicts with others. + */ + public static void showScheduleConflictDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true); + } + + /** + * Shows the conflict dialog for the channel watching. + */ + public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) { + if (channel == null) { + return; + } + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); + showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args); + } + + /** + * Shows DVR insufficient space error dialog. + */ + public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, + Set failedScheduledRecordingInfoSet) { + Bundle args = new Bundle(); + ArrayList 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) { + showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); + } + + /** + * Shows DVR small sized storage error dialog. + */ + public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); + } + + /** + * Shows stop recording dialog. + */ + public static void showStopRecordingDialog(Activity activity, long channelId, int reason, + HalfSizedDialogFragment.OnActionClickListener listener) { + Bundle args = new Bundle(); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); + args.putInt(DvrStopRecordingFragment.KEY_REASON, reason); + DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment(); + fragment.setOnActionClickListener(listener); + showDialogFragment(activity, fragment, args); + } + + /** + * Shows "already scheduled" dialog. + */ + public static void showAlreadyScheduleDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true); + } + + /** + * Shows "already recorded" dialog. + */ + public static void showAlreadyRecordedDialog(Activity activity, Program program) { + if (program == null) { + return; + } + Bundle args = new Bundle(); + args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + 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); + } + + private static void showDialogFragment(Activity activity, + DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory, + boolean keepProgramGuide) { + dialogFragment.setArguments(args); + if (activity instanceof MainActivity) { + ((MainActivity) activity).getOverlayManager() + .showDialogFragment(DvrHalfSizedDialogFragment.DIALOG_TAG, dialogFragment, + keepSidePanelHistory, keepProgramGuide); + } else { + dialogFragment.show(activity.getFragmentManager(), + DvrHalfSizedDialogFragment.DIALOG_TAG); + } + } + + /** + * Checks whether channel watch conflict dialog is open or not. + */ + public static boolean isChannelWatchConflictDialogShown(MainActivity activity) { + return activity.getOverlayManager().getCurrentDialog() instanceof + DvrChannelWatchConflictDialogFragment; + } + + private static ScheduledRecording getEarliestScheduledRecording(List + recordings) { + ScheduledRecording earlistScheduledRecording = null; + if (!recordings.isEmpty()) { + Collections.sort(recordings, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + earlistScheduledRecording = recordings.get(0); + } + return earlistScheduledRecording; + } + + /** + * Shows the schedules activity to resolve the tune conflict. + */ + public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) { + if (channel == null) { + return; + } + List conflicts = TvApplication.getSingletons(context).getDvrManager() + .getConflictingSchedulesForTune(channel.getId()); + startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); + } + + /** + * Shows the schedules activity to resolve the one time recording conflict. + */ + public static void startSchedulesActivityForOneTimeRecordingConflict(Context context, + List conflicts) { + startSchedulesActivity(context, getEarliestScheduledRecording(conflicts)); + } + + /** + * Shows the schedules activity with full schedule. + */ + public static void startSchedulesActivity(Context context, ScheduledRecording + focusedScheduledRecording) { + Intent intent = new Intent(context, DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, + DvrSchedulesActivity.TYPE_FULL_SCHEDULE); + if (focusedScheduledRecording != null) { + intent.putExtra(DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING, + focusedScheduledRecording); + } + context.startActivity(intent); + } + + /** + * Shows the schedules activity for series recording. + */ + public static void startSchedulesActivityForSeries(Context context, + SeriesRecording seriesRecording) { + Intent intent = new Intent(context, DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, + DvrSchedulesActivity.TYPE_SERIES_SCHEDULE); + intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, + seriesRecording); + context.startActivity(intent); + } + + /** + * Shows the series settings activity. + * + * @param programs list of programs which belong to the series. + */ + public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, + @Nullable List 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 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 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); + 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); + } + + /** + * Shows "series recording scheduled" dialog activity. + */ + public static void StartSeriesScheduledDialogActivity(Context context, + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, + List programs) { + if (seriesRecording == null) { + return; + } + Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); + intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, + seriesRecording.getId()); + intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, + showViewScheduleOptionInDialog); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, + programs); + context.startActivity(intent); + } + + /** + * Shows the details activity for the DVR items. The type of DVR items may be + * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. + */ + public static void startDetailsActivity(Activity activity, Object dvrItem, + @Nullable ImageView imageView, boolean hideViewSchedule) { + if (dvrItem == null) { + return; + } + Intent intent = new Intent(activity, DvrDetailsActivity.class); + long recordingId; + int viewType; + if (dvrItem instanceof ScheduledRecording) { + ScheduledRecording schedule = (ScheduledRecording) dvrItem; + recordingId = schedule.getId(); + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + } else { + return; + } + } else if (dvrItem instanceof RecordedProgram) { + recordingId = ((RecordedProgram) dvrItem).getId(); + viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + } else if (dvrItem instanceof SeriesRecording) { + recordingId = ((SeriesRecording) dvrItem).getId(); + viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; + } else { + return; + } + intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); + intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); + intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); + Bundle bundle = null; + if (imageView != null) { + bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, + DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); + } + activity.startActivity(intent, bundle); + } + + /** + * Shows the cancel all dialog for series schedules list. + */ + public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, + SeriesRecording seriesRecording) { + DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = + new DvrStopSeriesRecordingDialogFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, + seriesRecording); + dvrStopSeriesRecordingDialogFragment.setArguments(arguments); + dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), + DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); + } + + /** + * Shows the series deletion activity. + */ + public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) { + Intent intent = new Intent(context, DvrSeriesDeletionActivity.class); + intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId); + context.startActivity(intent); + } + + public static void showAddScheduleToast(Context context, + String title, long startTimeMs, long endTimeMs) { + String msg = (startTimeMs > System.currentTimeMillis()) ? + context.getString(R.string.dvr_msg_program_scheduled, title) + : context.getString(R.string.dvr_msg_current_program_scheduled, title, + Utils.toTimeString(endTimeMs, false)); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } +} \ 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/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java deleted file mode 100644 index d4d4d8ab..00000000 --- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java +++ /dev/null @@ -1,29 +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; - -/** - * Special object for schedule preview; - */ -final class FullScheduleCardHolder { - /** - * Full schedule card holder. - */ - static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder(); - - private FullScheduleCardHolder() { } -} diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java deleted file mode 100644 index 7dd85f45..00000000 --- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java +++ /dev/null @@ -1,84 +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.Context; -import android.support.v17.leanback.widget.Presenter; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; - -import java.util.Collections; -import java.util.List; - -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ -public class FullSchedulesCardPresenter extends Presenter { - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); - - cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule)); - cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title)); - List scheduledRecordings = TvApplication.getSingletons(context) - .getDvrDataManager().getAvailableScheduledRecordings(); - int fullDays = 0; - if (!scheduledRecordings.isEmpty()) { - fullDays = Utils.computeDateDifference(System.currentTimeMillis(), - Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) - .getStartTimeMs()) + 1; - } - cardView.setContent(context.getResources().getQuantityString( - R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); - - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - DvrUiHelper.startSchedulesActivity(context, null); - } - }; - baseHolder.view.setOnClickListener(clickListener); - } - - @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.reset(); - } - - private static final class ScheduledRecordingViewHolder extends ViewHolder { - ScheduledRecordingViewHolder(RecordingCardView view) { - super(view); - } - } -} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java deleted file mode 100644 index d320816e..00000000 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ /dev/null @@ -1,117 +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.DialogInterface; -import android.os.Bundle; -import android.os.Handler; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dialog.SafeDismissDialogFragment; - -import java.util.concurrent.TimeUnit; - -public class HalfSizedDialogFragment extends SafeDismissDialogFragment { - public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); - public static final String TRACKER_LABEL = "Half sized dialog"; - - private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); - - private OnActionClickListener mOnActionClickListener; - - private Handler mHandler = new Handler(); - private Runnable mAutoDismisser = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.halfsized_dialog, container, false); - } - - @Override - public void onStart() { - super.onStart(); - getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - } - - @Override - public void onPause() { - super.onPause(); - if (mOnActionClickListener != null) { - // Dismisses the dialog to prevent the callback being forgotten during - // fragment re-creating. - dismiss(); - } - } - - @Override - public void onStop() { - super.onStop(); - mHandler.removeCallbacks(mAutoDismisser); - } - - @Override - public int getTheme() { - return R.style.Theme_TV_dialog_HalfSizedDialog; - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - /** - * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog - * will be automatically closed when it's paused to prevent the fragment being re-created by - * the framework, which will result the listener being forgotten. - */ - public void setOnActionClickListener(OnActionClickListener listener) { - mOnActionClickListener = listener; - } - - /** - * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. - */ - protected OnActionClickListener getOnActionClickListener() { - return mOnActionClickListener; - } - - /** - * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments - * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier - * of the action user clicked. - */ - public interface OnActionClickListener { - void onActionClick(long actionId); - } -} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java deleted file mode 100644 index 158bd824..00000000 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ /dev/null @@ -1,251 +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.FragmentManager; -import android.content.Context; -import android.graphics.Typeface; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.SeriesRecording; - -import java.util.ArrayList; -import java.util.List; - -/** - * Fragment for DVR series recording settings. - */ -public class PrioritySettingsFragment extends GuidedStepFragment { - /** - * Name of series recording id starting the fragment. - * Type: Long - */ - public static final String COME_FROM_SERIES_RECORDING_ID = "series_recording_id"; - - private static final int ONE_TIME_RECORDING_ID = 0; - // button action's IDs are negative. - private static final long ACTION_ID_SAVE = -100L; - - private final List mSeriesRecordings = new ArrayList<>(); - - private SeriesRecording mSelectedRecording; - private SeriesRecording mComeFromSeriesRecording; - private float mSelectedActionElevation; - private int mActionColor; - private int mSelectedActionColor; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mSeriesRecordings.clear(); - mSeriesRecordings.add(new SeriesRecording.Builder() - .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) - .setPriority(Long.MAX_VALUE) - .setId(ONE_TIME_RECORDING_ID) - .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - long comeFromSeriesRecordingId = - getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); - for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { - if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL - || series.getId() == comeFromSeriesRecordingId) { - mSeriesRecordings.add(series); - } - } - mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); - mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); - mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); - mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); - mSelectedActionColor = - getResources().getColor(R.color.dvr_guided_step_action_text_color_selected, null); - } - - @Override - public void onResume() { - super.onResume(); - setSelectedActionPosition(mComeFromSeriesRecording == null ? 1 - : mSeriesRecordings.indexOf(mComeFromSeriesRecording)); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = mComeFromSeriesRecording == null ? null - : mComeFromSeriesRecording.getTitle(); - return new Guidance(getString(R.string.dvr_priority_title), - getString(R.string.dvr_priority_description), breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - int position = 0; - for (SeriesRecording seriesRecording : mSeriesRecordings) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(position++) - .title(seriesRecording.getTitle()) - .build()); - } - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SAVE) - .title(getString(R.string.dvr_priority_button_action_save)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == ACTION_ID_SAVE) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - int size = mSeriesRecordings.size(); - for (int i = 1; i < size; ++i) { - long priority = DvrScheduleManager.suggestSeriesPriority(size - i); - SeriesRecording seriesRecording = mSeriesRecordings.get(i); - if (seriesRecording.getPriority() != priority) { - dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(seriesRecording) - .setPriority(priority).build()); - } - } - FragmentManager fragmentManager = getFragmentManager(); - fragmentManager.popBackStack(); - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - FragmentManager fragmentManager = getFragmentManager(); - fragmentManager.popBackStack(); - } else if (mSelectedRecording == null) { - mSelectedRecording = mSeriesRecordings.get((int) actionId); - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - } else { - mSelectedRecording = null; - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - } - } - - @Override - public void onGuidedActionFocused(GuidedAction action) { - super.onGuidedActionFocused(action); - if (mSelectedRecording == null) { - return; - } - if (action.getId() < 0) { - int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording); - mSelectedRecording = null; - for (int i = 0; i < mSeriesRecordings.size(); ++i) { - updateItem(i); - } - return; - } - int position = (int) action.getId(); - int previousPosition = mSeriesRecordings.indexOf(mSelectedRecording); - mSeriesRecordings.remove(mSelectedRecording); - mSeriesRecordings.add(position, mSelectedRecording); - updateItem(previousPosition); - updateItem(position); - notifyActionChanged(previousPosition); - notifyActionChanged(position); - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new DvrGuidedActionsStylist(false) { - @Override - public void onBindViewHolder(ViewHolder vh, GuidedAction action) { - super.onBindViewHolder(vh, action); - updateItem(vh.itemView, (int) action.getId()); - } - - @Override - public int onProvideItemLayoutId() { - return R.layout.priority_settings_action_item; - } - }; - } - - private void updateItem(int position) { - View itemView = getActionItemView(position); - if (itemView == null) { - return; - } - updateItem(itemView, position); - } - - private void updateItem(View itemView, int position) { - GuidedAction action = getActions().get(position); - action.setTitle(mSeriesRecordings.get(position).getTitle()); - boolean selected = mSelectedRecording != null - && mSeriesRecordings.indexOf(mSelectedRecording) == position; - TextView titleView = (TextView) itemView.findViewById(R.id.guidedactions_item_title); - ImageView imageView = (ImageView) itemView.findViewById(R.id.guidedactions_item_tail_image); - if (position == 0) { - // one-time recording - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setVisibility(View.GONE); - itemView.setFocusable(false); - itemView.setElevation(0); - // strings.xml tag doesn't work. - titleView.setTypeface(titleView.getTypeface(), Typeface.ITALIC); - } else if (mSelectedRecording == null) { - titleView.setTextColor(mActionColor); - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setImageResource(R.drawable.ic_draggable_white); - imageView.setVisibility(View.VISIBLE); - itemView.setFocusable(true); - itemView.setElevation(0); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } else if (selected) { - titleView.setTextColor(mSelectedActionColor); - itemView.setBackgroundResource(R.drawable.priority_settings_action_item_selected); - imageView.setImageResource(R.drawable.ic_dragging_grey); - imageView.setVisibility(View.VISIBLE); - itemView.setFocusable(true); - itemView.setElevation(mSelectedActionElevation); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } else { - titleView.setTextColor(mActionColor); - itemView.setBackgroundResource(R.drawable.setup_selector_background); - imageView.setVisibility(View.INVISIBLE); - itemView.setFocusable(true); - itemView.setElevation(0); - titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); - } - } -} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java deleted file mode 100644 index e698b8a2..00000000 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ /dev/null @@ -1,170 +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.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; - -/** - * {@link DetailsFragment} for recorded program in DVR. - */ -public class RecordedProgramDetailsFragment extends DvrDetailsFragment - implements DvrDataManager.RecordedProgramListener { - private static final int ACTION_RESUME_PLAYING = 1; - private static final int ACTION_PLAY_FROM_BEGINNING = 2; - private static final int ACTION_DELETE_RECORDING = 3; - - private DvrWatchedPositionManager mDvrWatchedPositionManager; - - private RecordedProgram mRecordedProgram; - private DetailsContent mDetailsContent; - private boolean mPaused; - private DvrDataManager mDvrDataManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); - mDvrDataManager.addRecordedProgramListener(this); - super.onCreate(savedInstanceState); - } - - @Override - public void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); - } - - @Override - public void onResume() { - super.onResume(); - if (mPaused) { - updateActions(); - mPaused = false; - } - } - - @Override - public void onPause() { - super.onPause(); - mPaused = true; - } - - @Override - public void onDestroy() { - mDvrDataManager.removeRecordedProgramListener(this); - super.onDestroy(); - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); - if (mRecordedProgram == null) { - // notify super class to end activity before initializing anything - return false; - } - mDetailsContent = createDetailsContent(); - return true; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecordedProgram.getChannelId()); - String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription()) - ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(getTitleFromProgram(mRecordedProgram, channel)) - .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis()) - .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis()) - .setDescription(description) - .setImageUris(mRecordedProgram, channel) - .build(); - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) - == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, - res.getString(R.string.dvr_detail_resume_play), null, - res.getDrawable(R.drawable.lb_ic_play))); - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_play_from_beginning), null, - res.getDrawable(R.drawable.lb_ic_replay))); - } else { - adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, - res.getString(R.string.dvr_detail_watch), null, - res.getDrawable(R.drawable.lb_ic_play))); - } - adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING, - res.getString(R.string.dvr_detail_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { - startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); - } else if (action.getId() == ACTION_RESUME_PLAYING) { - startPlayback(mRecordedProgram, mDvrWatchedPositionManager - .getWatchedPosition(mRecordedProgram.getId())); - } else if (action.getId() == ACTION_DELETE_RECORDING) { - DvrManager dvrManager = TvApplication - .getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedProgram(mRecordedProgram); - getActivity().finish(); - } - } - }; - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (recordedProgram.getId() == mRecordedProgram.getId()) { - getActivity().finish(); - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java deleted file mode 100644 index 1bf34310..00000000 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ /dev/null @@ -1,182 +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.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.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. - */ -public class RecordedProgramPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; - private final DvrWatchedPositionManager mDvrWatchedPositionManager; - private final Context mContext; - private String mTodayString; - private String mYesterdayString; - private final int mProgressBarColor; - private final boolean mShowEpisodeTitle; - - private static final class RecordedProgramViewHolder extends ViewHolder - implements WatchedPositionChangedListener { - private RecordedProgram mProgram; - - RecordedProgramViewHolder(RecordingCardView view, int progressColor) { - super(view); - view.setProgressBarColor(progressColor); - } - - private void setProgram(RecordedProgram program) { - mProgram = program; - } - - private void setProgressBar(long watchedPositionMs) { - ((RecordingCardView) view).setProgressBar( - (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null - : Math.min(100, (int) (100.0f * watchedPositionMs - / mProgram.getDurationMillis()))); - } - - @Override - public void onWatchedPositionChanged(long programId, long positionMs) { - if (programId == mProgram.getId()) { - setProgressBar(positionMs); - } - } - } - - public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) { - mContext = context; - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - mTodayString = context.getString(R.string.dvr_date_today); - mYesterdayString = context.getString(R.string.dvr_date_yesterday); - mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_progress_bar_watched); - mShowEpisodeTitle = showEpisodeTitle; - } - - public RecordedProgramPresenter(Context context) { - this(context, false); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext); - return new RecordedProgramViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - final RecordedProgram program = (RecordedProgram) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) - : program.getTitleWithEpisodeNumber(mContext); - SpannableString title = titleString == null ? null : new SpannableString(titleString); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else if (!mShowEpisodeTitle) { - // TODO: Some translation may add delimiters in-between program titles, we should use - // a more robust way to get the span range. - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - cardView.setTitle(title); - String imageUri = null; - boolean isChannelLogo = false; - if (program.getPosterArtUri() != null) { - imageUri = program.getPosterArtUri(); - } else if (program.getThumbnailUri() != null) { - imageUri = program.getThumbnailUri(); - } else if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - isChannelLogo = true; - } - cardView.setImageUri(imageUri, isChannelLogo); - int durationMinutes = - Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis())); - String durationString = getContext().getResources().getQuantityString( - R.plurals.dvr_program_duration, durationMinutes, durationMinutes); - cardView.setContent(getDescription(program), durationString); - if (viewHolder instanceof RecordedProgramViewHolder) { - RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; - cardViewHolder.setProgram(program); - mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId()); - cardViewHolder - .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); - } - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - if (viewHolder instanceof RecordedProgramViewHolder) { - mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder, - ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); - } - ((RecordingCardView) viewHolder.view).reset(); - super.onUnbindViewHolder(viewHolder); - } - - /** - * Returns description would be used in its card view. - */ - protected String getDescription(RecordedProgram recording) { - int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(), - System.currentTimeMillis()); - if (dateDifference == 0) { - return mTodayString; - } else if (dateDifference == 1) { - return mYesterdayString; - } else { - return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(), - recording.getStartTimeUtcMillis(), false, true, false, 0); - } - } - - /** - * Returns context. - */ - protected Context getContext() { - return mContext; - } -} diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java deleted file mode 100644 index 51c3b03b..00000000 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ /dev/null @@ -1,185 +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.Context; -import android.graphics.Bitmap; -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.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.util.ImageLoader; - -/** - * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or - * {@link RecordedProgram} or - * {@link com.android.tv.dvr.SeriesRecording}. - */ -class RecordingCardView extends BaseCardView { - 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; - - RecordingCardView(Context context) { - this(context, - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width), - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height)); - } - - RecordingCardView(Context context, int imageWidth, int imageHeight) { - super(context); - //TODO(dvr): move these to the layout XML. - setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); - setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); - setFocusable(true); - setFocusableInTouchMode(true); - mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null); - - LayoutInflater inflater = LayoutInflater.from(getContext()); - inflater.inflate(R.layout.dvr_recording_card_view, this); - mImageView = (ImageView) findViewById(R.id.image); - mImageWidth = imageWidth; - mImageHeight = imageHeight; - mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); - mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); - mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); - mTitleView = (TextView) findViewById(R.id.title); - mMajorContentView = (TextView) findViewById(R.id.content_major); - mMinorContentView = (TextView) findViewById(R.id.content_minor); - } - - void setTitle(CharSequence title) { - mTitleView.setText(title); - } - - void setContent(CharSequence majorContent, CharSequence minorContent) { - if (!TextUtils.isEmpty(majorContent)) { - mMajorContentView.setText(majorContent); - mMajorContentView.setVisibility(View.VISIBLE); - } else { - mMajorContentView.setVisibility(View.GONE); - } - if (!TextUtils.isEmpty(minorContent)) { - mMinorContentView.setText(minorContent); - mMinorContentView.setVisibility(View.VISIBLE); - } else { - mMinorContentView.setVisibility(View.GONE); - } - } - - /** - * Sets progress bar. If progress is {@code null}, hides progress bar. - */ - void setProgressBar(Integer progress) { - if (progress == null) { - mProgressBar.setVisibility(View.GONE); - } else { - mProgressBar.setProgress(progress); - mProgressBar.setVisibility(View.VISIBLE); - } - } - - /** - * Sets the color of progress bar. - */ - void setProgressBarColor(int color) { - mProgressBar.getProgressDrawable().setTint(color); - } - - void setImageUri(String uri, boolean isChannelLogo) { - if (isChannelLogo) { - mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - } else { - mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); - } - mImageUri = uri; - if (TextUtils.isEmpty(uri)) { - mImageView.setImageDrawable(mDefaultImage); - } else { - ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, - new RecordingCardImageLoaderCallback(this, uri)); - } - } - - /** - * Set image to card view. - */ - public void setImage(Drawable image) { - if (image != null) { - mImageView.setImageDrawable(image); - } - } - - public void setAffiliatedIcon(int imageResId) { - if (imageResId > 0) { - mAffiliatedIconContainer.setVisibility(View.VISIBLE); - mAffiliatedIcon.setImageResource(imageResId); - } else { - mAffiliatedIconContainer.setVisibility(View.INVISIBLE); - } - } - - /** - * Returns image view. - */ - public ImageView getImageView() { - return mImageView; - } - - private static class RecordingCardImageLoaderCallback - extends ImageLoader.ImageLoaderCallback { - private final String mUri; - - RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) { - super(referent); - mUri = uri; - } - - @Override - public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) { - if (bitmap == null || !mUri.equals(view.mImageUri)) { - view.mImageView.setImageDrawable(view.mDefaultImage); - } else { - view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap)); - } - } - } - - public void reset() { - mTitleView.setText(null); - setContent(null, null); - mImageView.setImageDrawable(mDefaultImage); - } -} diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java deleted file mode 100644 index 4e19ec3f..00000000 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.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.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.dvr.ScheduledRecording; - -/** - * {@link DetailsFragment} for recordings in DVR. - */ -abstract class RecordingDetailsFragment extends DvrDetailsFragment { - private ScheduledRecording mRecording; - - @Override - protected void onCreateInternal() { - setDetailsOverviewRow(createDetailsContent()); - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() - .getScheduledRecording(scheduledRecordingId); - return mRecording != null; - } - - /** - * Returns {@link ScheduledRecording} for the current fragment. - */ - public ScheduledRecording getRecording() { - return mRecording; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecording.getChannelId()); - SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ? - null : new SpannableString(mRecording - .getProgramTitleWithEpisodeNumber(getContext())); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = mRecording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ? - mRecording.getProgramDescription() : mRecording.getProgramLongDescription(); - if (TextUtils.isEmpty(description)) { - description = channel != null ? channel.getDescription() : null; - } - return new DetailsContent.Builder() - .setTitle(title) - .setStartTimeUtcMillis(mRecording.getStartTimeMs()) - .setEndTimeUtcMillis(mRecording.getEndTimeMs()) - .setDescription(description) - .setImageUris(mRecording.getProgramPosterArtUri(), - mRecording.getProgramThumbnailUri(), channel) - .build(); - } -} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java deleted file mode 100644 index 60816bb5..00000000 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ /dev/null @@ -1,97 +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.os.Bundle; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; - -/** - * {@link RecordingDetailsFragment} for scheduled recording in DVR. - */ -public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_VIEW_SCHEDULE = 1; - private static final int ACTION_CANCEL = 2; - - private DvrManager mDvrManager; - private Action mScheduleAction; - private boolean mHideViewSchedule; - - @Override - public void onCreate(Bundle savedInstance) { - mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); - super.onCreate(savedInstance); - } - - @Override - public void onResume() { - super.onResume(); - if (mScheduleAction != null) { - mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId())); - } - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - if (!mHideViewSchedule) { - mScheduleAction = new Action(ACTION_VIEW_SCHEDULE, - res.getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(getScheduleIconId())); - adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); - } - adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, - res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null, - res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - long actionId = action.getId(); - if (actionId == ACTION_VIEW_SCHEDULE) { - DvrUiHelper.startSchedulesActivity(getContext(), getRecording()); - } else if (actionId == ACTION_CANCEL) { - mDvrManager.removeScheduledRecording(getRecording()); - getActivity().finish(); - } - } - }; - } - - private int getScheduleIconId() { - if (mDvrManager.isConflicting(getRecording())) { - return R.drawable.ic_warning_white_32dp; - } else { - return R.drawable.ic_schedule_32dp; - } - } -} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java deleted file mode 100644 index 5f447f13..00000000 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ /dev/null @@ -1,177 +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.content.Context; -import android.media.tv.TvContract; -import android.os.Handler; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.view.ViewGroup; - -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.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ -public class ScheduledRecordingPresenter extends DvrItemPresenter { - private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); - - private final ChannelDataManager mChannelDataManager; - private final DvrManager mDvrManager; - private final Context mContext; - private final int mProgressBarColor; - - private static final class ScheduledRecordingViewHolder extends ViewHolder { - private final Handler mHandler = new Handler(); - private ScheduledRecording mScheduledRecording; - private final Runnable mProgressBarUpdater = new Runnable() { - @Override - public void run() { - updateProgressBar(); - mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); - } - }; - - ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { - super(view); - view.setProgressBarColor(progressBarColor); - } - - private void updateProgressBar() { - if (mScheduledRecording == null) { - return; - } - int recordingState = mScheduledRecording.getState(); - RecordingCardView cardView = (RecordingCardView) view; - if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - cardView.setProgressBar(Math.max(0, Math.min((int) (100 * - (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) - / mScheduledRecording.getDuration()), 100))); - } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { - cardView.setProgressBar(100); - } else { - // Hides progress bar. - cardView.setProgressBar(null); - } - } - - private void startUpdateProgressBar() { - mHandler.post(mProgressBarUpdater); - } - - private void stopUpdateProgressBar() { - mHandler.removeCallbacks(mProgressBarUpdater); - } - } - - public ScheduledRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); - mDvrManager = singletons.getDvrManager(); - mContext = context; - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_recording_icon_color_on_focus); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final ScheduledRecording recording = (ScheduledRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); - - setTitleAndImage(cardView, recording); - int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), - recording.getStartTimeMs()); - if (dateDifference <= 0) { - cardView.setContent(mContext.getString(R.string.dvr_date_today_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else if (dateDifference == 1) { - cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else { - cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getStartTimeMs(), false, true, false, 0), null); - } - if (mDvrManager.isConflicting(recording)) { - cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); - } else { - cardView.setAffiliatedIcon(0); - } - viewHolder.updateProgressBar(); - viewHolder.mScheduledRecording = recording; - viewHolder.startUpdateProgressBar(); - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - viewHolder.stopUpdateProgressBar(); - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.mScheduledRecording = null; - cardView.reset(); - super.onUnbindViewHolder(viewHolder); - } - - private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ? - null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext)); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else { - String programTitle = recording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), - programTitle == null ? 0 : programTitle.length(), title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String imageUri = recording.getProgramPosterArtUri(); - boolean isChannelLogo = false; - if (TextUtils.isEmpty(imageUri)) { - imageUri = channel != null ? - TvContract.buildChannelLogoUri(channel.getId()).toString() : null; - isChannelLogo = true; - } - cardView.setTitle(title); - cardView.setImageUri(imageUri, isChannelLogo); - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java deleted file mode 100644 index 36e3cfc1..00000000 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ /dev/null @@ -1,252 +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.Context; -import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.text.TextUtils; -import android.view.ViewGroup.LayoutParams; -import android.widget.Toast; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.ui.GuidedActionsStylistWithDivider; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Fragment for DVR series recording settings. - */ -public class SeriesDeletionFragment extends GuidedStepFragment { - private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); - - // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, - // negative values are used by other actions to prevent duplicated IDs. - private static final long ACTION_ID_SELECT_WATCHED = -110; - private static final long ACTION_ID_SELECT_ALL = -111; - private static final long ACTION_ID_DELETE = -112; - - private DvrDataManager mDvrDataManager; - private DvrWatchedPositionManager mDvrWatchedPositionManager; - private List mRecordings; - private final Set mWatchedRecordings = new HashSet<>(); - private boolean mAllSelected; - private long mSeriesRecordingId; - private int mOneLineActionHeight; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mSeriesRecordingId = getArguments() - .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); - SoftPreconditions.checkArgument(mSeriesRecordingId != -1); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); - mOneLineActionHeight = getResources().getDimensionPixelSize( - R.dimen.dvr_settings_one_line_action_container_height); - if (mRecordings.isEmpty()) { - Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings), - Toast.LENGTH_LONG).show(); - finishGuidedStepFragments(); - return; - } - Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = null; - SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); - if (series != null) { - breadcrumb = series.getTitle(); - } - return new Guidance(getString(R.string.dvr_series_deletion_title), - getString(R.string.dvr_series_deletion_description), breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_WATCHED) - .title(getString(R.string.dvr_series_select_watched)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_SELECT_ALL) - .title(getString(R.string.dvr_series_select_all)) - .build()); - actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); - for (RecordedProgram recording : mRecordings) { - long watchedPositionMs = - mDvrWatchedPositionManager.getWatchedPosition(recording.getId()); - String title = recording.getEpisodeDisplayTitle(getContext()); - if (TextUtils.isEmpty(title)) { - title = TextUtils.isEmpty(recording.getTitle()) ? - getString(R.string.channel_banner_no_title) : recording.getTitle(); - } - String description; - if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - description = getWatchedString(watchedPositionMs, recording.getDurationMillis()); - mWatchedRecordings.add(recording.getId()); - } else { - description = getString(R.string.dvr_series_never_watched); - } - actions.add(new GuidedAction.Builder(getActivity()) - .id(recording.getId()) - .title(title) - .description(description) - .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID) - .build()); - } - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_DELETE) - .title(getString(R.string.dvr_detail_delete)) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == ACTION_ID_DELETE) { - List idsToDelete = new ArrayList<>(); - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID - && guidedAction.isChecked()) { - idsToDelete.add(guidedAction.getId()); - } - } - if (!idsToDelete.isEmpty()) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedPrograms(idsToDelete); - } - Toast.makeText(getContext(), getResources().getQuantityString( - R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), - mRecordings.size()), Toast.LENGTH_LONG).show(); - finishGuidedStepFragments(); - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - finishGuidedStepFragments(); - } else if (actionId == ACTION_ID_SELECT_WATCHED) { - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - long recordingId = guidedAction.getId(); - if (mWatchedRecordings.contains(recordingId)) { - guidedAction.setChecked(true); - } else { - guidedAction.setChecked(false); - } - notifyActionChanged(findActionPositionById(recordingId)); - } - } - mAllSelected = updateSelectAllState(); - } else if (actionId == ACTION_ID_SELECT_ALL) { - mAllSelected = !mAllSelected; - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - guidedAction.setChecked(mAllSelected); - notifyActionChanged(findActionPositionById(guidedAction.getId())); - } - } - updateSelectAllState(action, mAllSelected); - } else { - mAllSelected = updateSelectAllState(); - } - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - @Override - public GuidedActionsStylist onCreateActionsStylist() { - return new GuidedActionsStylistWithDivider() { - @Override - public void onBindViewHolder(ViewHolder vh, GuidedAction action) { - super.onBindViewHolder(vh, action); - if (action.getId() == ACTION_DIVIDER) { - return; - } - LayoutParams lp = vh.itemView.getLayoutParams(); - if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) { - lp.height = mOneLineActionHeight; - } else { - vh.itemView.setLayoutParams( - new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT)); - } - } - }; - } - - private String getWatchedString(long watchedPositionMs, long durationMs) { - if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { - return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)), - TimeUnit.MILLISECONDS.toMinutes(durationMs)); - } else { - return getResources().getString(R.string.dvr_series_watched_info_seconds, - Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), - TimeUnit.MILLISECONDS.toSeconds(durationMs)); - } - } - - private boolean updateSelectAllState() { - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) { - if (!guidedAction.isChecked()) { - if (mAllSelected) { - updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false); - } - return false; - } - } - } - if (!mAllSelected) { - updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true); - } - return true; - } - - private void updateSelectAllState(GuidedAction selectAll, boolean select) { - selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all) - : getString(R.string.dvr_series_select_all)); - notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL)); - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java deleted file mode 100644 index e9e391d4..00000000 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ /dev/null @@ -1,375 +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.graphics.drawable.Drawable; -import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.DetailsOverviewRow; -import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.OnActionClickedListener; -import android.support.v17.leanback.widget.PresenterSelector; -import android.support.v17.leanback.widget.SparseArrayObjectAdapter; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -/** - * {@link DetailsFragment} for series recording in DVR. - */ -public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements - DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { - private static final int ACTION_WATCH = 1; - private static final int ACTION_SERIES_SCHEDULES = 2; - private static final int ACTION_DELETE = 3; - - private DvrWatchedPositionManager mDvrWatchedPositionManager; - private DvrDataManager mDvrDataManager; - - private SeriesRecording mSeries; - // NOTICE: mRecordedPrograms should only be used in creating details fragments. - // After fragments are created, it should be cleared to save resources. - private List mRecordedPrograms; - private RecordedProgram mRecommendRecordedProgram; - private DetailsContent mDetailsContent; - private int mSeasonRowCount; - private SparseArrayObjectAdapter mActionsAdapter; - private Action mDeleteAction; - - private boolean mPaused; - private long mInitialPlaybackPositionMs; - private String mWatchLabel; - private String mResumeLabel; - private Drawable mWatchDrawable; - private RecordedProgramPresenter mRecordedProgramPresenter; - - @Override - public void onCreate(Bundle savedInstanceState) { - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mWatchLabel = getString(R.string.dvr_detail_watch); - mResumeLabel = getString(R.string.dvr_detail_series_resume); - mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); - mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); - super.onCreate(savedInstanceState); - } - - @Override - protected void onCreateInternal() { - mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) - .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); - setupRecordedProgramsRow(); - mDvrDataManager.addSeriesRecordingListener(this); - mDvrDataManager.addRecordedProgramListener(this); - mRecordedPrograms = null; - } - - @Override - public void onResume() { - super.onResume(); - if (mPaused) { - updateWatchAction(); - mPaused = false; - } - } - - @Override - public void onPause() { - super.onPause(); - mPaused = true; - } - - private void updateWatchAction() { - List programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); - Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); - mRecommendRecordedProgram = getRecommendProgram(programs); - if (mRecommendRecordedProgram == null) { - mActionsAdapter.clear(ACTION_WATCH); - } else { - String episodeStatus; - if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) - == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - episodeStatus = mResumeLabel; - mInitialPlaybackPositionMs = mDvrWatchedPositionManager - .getWatchedPosition(mRecommendRecordedProgram.getId()); - } else { - episodeStatus = mWatchLabel; - mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - } - String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( - getContext()); - mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, - episodeStatus, episodeDisplayNumber, mWatchDrawable)); - } - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() - .getSeriesRecording(recordId); - if (mSeries == null) { - return false; - } - mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); - Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); - mDetailsContent = createDetailsContent(); - return true; - } - - @Override - protected PresenterSelector onCreatePresenterSelector( - DetailsOverviewRowPresenter rowPresenter) { - ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); - return presenterSelector; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mSeries.getChannelId()); - String description = TextUtils.isEmpty(mSeries.getLongDescription()) - ? mSeries.getDescription() : mSeries.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(mSeries.getTitle()) - .setDescription(description) - .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel) - .build(); - } - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - updateWatchAction(); - mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, - getString(R.string.dvr_detail_view_schedule), null, - res.getDrawable(R.drawable.ic_schedule_32dp, null))); - mDeleteAction = new Action(ACTION_DELETE, - getString(R.string.dvr_detail_series_delete), null, - res.getDrawable(R.drawable.ic_delete_32dp, null)); - if (!mRecordedPrograms.isEmpty()) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); - } - return mActionsAdapter; - } - - private void setupRecordedProgramsRow() { - for (RecordedProgram program : mRecordedPrograms) { - addProgram(program); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - mDvrDataManager.removeSeriesRecordingListener(this); - mDvrDataManager.removeRecordedProgramListener(this); - if (mSeries != null) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { - dvrManager.removeSeriesRecording(mSeries.getId()); - } - } - mRecordedProgramPresenter.unbindAllViewHolders(); - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_WATCH) { - startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); - } else if (action.getId() == ACTION_SERIES_SCHEDULES) { - DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); - } else if (action.getId() == ACTION_DELETE) { - DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); - } - } - }; - } - - /** - * The programs are sorted by season number and episode number. - */ - private RecordedProgram getRecommendProgram(List programs) { - for (int i = programs.size() - 1 ; i >= 0 ; i--) { - RecordedProgram program = programs.get(i); - int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); - if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { - continue; - } - if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { - return program; - } - if (i == programs.size() - 1) { - return program; - } else { - return programs.get(i + 1); - } - } - return programs.isEmpty() ? null : programs.get(0); - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - for (SeriesRecording series : seriesRecordings) { - if (mSeries.getId() == series.getId()) { - mSeries = series; - } - } - } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { - for (SeriesRecording series : seriesRecordings) { - if (series.getId() == mSeries.getId()) { - mSeries = null; - getActivity().finish(); - return; - } - } - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - addProgram(recordedProgram); - if (mActionsAdapter.lookup(ACTION_DELETE) == null) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); - } - } - } - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - // Do nothing - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); - if (row != null) { - SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); - adapter.remove(recordedProgram); - if (adapter.isEmpty()) { - getRowsAdapter().remove(row); - if (getRowsAdapter().size() == 1) { - // No season rows left. Only DetailsOverviewRow - mActionsAdapter.clear(ACTION_DELETE); - } - } - } - if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { - updateWatchAction(); - } - } - } - } - - private void addProgram(RecordedProgram program) { - String programSeasonNumber = - TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber(); - getOrCreateSeasonRowAdapter(programSeasonNumber).add(program); - } - - private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) { - ListRow row = getSeasonRow(seasonNumber, true); - return (SeasonRowAdapter) row.getAdapter(); - } - - private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) { - seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber; - ArrayObjectAdapter rowsAdaptor = getRowsAdapter(); - for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { - Object row = rowsAdaptor.get(i); - if (row instanceof ListRow) { - int compareResult = BaseProgram.numberCompare(seasonNumber, - ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); - if (compareResult == 0) { - return (ListRow) row; - } else if (compareResult < 0) { - return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null; - } - } - } - return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null; - } - - private ListRow createNewSeasonRow(String seasonNumber, int position) { - String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle() - : getString(R.string.dvr_detail_series_season_title, seasonNumber); - HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); - ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); - ListRow row = new ListRow(header, new SeasonRowAdapter(selector, - new Comparator() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); - } - }, seasonNumber)); - getRowsAdapter().add(position, row); - return row; - } - - private class SeasonRowAdapter extends SortedArrayAdapter { - private String mSeasonNumber; - - SeasonRowAdapter(PresenterSelector selector, Comparator comparator, - String seasonNumber) { - super(selector, comparator); - mSeasonNumber = seasonNumber; - } - - @Override - public long getId(RecordedProgram program) { - return program.getId(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java deleted file mode 100644 index c2c0f596..00000000 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ /dev/null @@ -1,234 +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.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.text.TextUtils; -import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; - -import java.util.List; - -/** - * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. - */ -public class SeriesRecordingPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; - private final DvrDataManager mDvrDataManager; - private final DvrManager mDvrManager; - private final DvrWatchedPositionManager mWatchedPositionManager; - - private static final class SeriesRecordingViewHolder extends ViewHolder implements - WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { - private SeriesRecording mSeriesRecording; - private RecordingCardView mCardView; - private DvrDataManager mDvrDataManager; - private DvrManager mDvrManager; - private DvrWatchedPositionManager mWatchedPositionManager; - - SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, - DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { - super(view); - mCardView = view; - mDvrDataManager = dvrDataManager; - mDvrManager = dvrManager; - mWatchedPositionManager = watchedPositionManager; - } - - @Override - public void onWatchedPositionChanged(long recordedProgramId, long positionMs) { - if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgramId); - updateCardViewContent(); - } - } - - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduledRecording : scheduledRecordings) { - if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { - updateCardViewContent(); - return; - } - } - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording scheduledRecording : scheduledRecordings) { - if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { - updateCardViewContent(); - return; - } - } - } - - @Override - public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { - boolean needToUpdateCardView = false; - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { - mDvrDataManager.removeScheduledRecordingListener(this); - mWatchedPositionManager.addListener(this, recordedProgram.getId()); - needToUpdateCardView = true; - } - } - if (needToUpdateCardView) { - updateCardViewContent(); - } - } - - @Override - public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { - boolean needToUpdateCardView = false; - for (RecordedProgram recordedProgram : recordedPrograms) { - if (TextUtils.equals(recordedProgram.getSeriesId(), - mSeriesRecording.getSeriesId())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgram.getId()); - } - needToUpdateCardView = true; - } - } - if (needToUpdateCardView) { - updateCardViewContent(); - } - } - - @Override - public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { - // Do nothing - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - // Do nothing - } - - public void onBound(SeriesRecording seriesRecording) { - mSeriesRecording = seriesRecording; - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addRecordedProgramListener(this); - for (RecordedProgram recordedProgram : - mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.addListener(this, recordedProgram.getId()); - } - } - updateCardViewContent(); - } - - public void onUnbound() { - mDvrDataManager.removeScheduledRecordingListener(this); - mDvrDataManager.removeRecordedProgramListener(this); - mWatchedPositionManager.removeListener(this); - } - - private void updateCardViewContent() { - int count = 0; - int quantityStringID; - List recordedPrograms = - mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); - if (recordedPrograms.size() == 0) { - count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); - quantityStringID = R.plurals.dvr_count_scheduled_recordings; - } else { - for (RecordedProgram recordedProgram : recordedPrograms) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - count++; - } - } - if (count == 0) { - count = recordedPrograms.size(); - quantityStringID = R.plurals.dvr_count_recordings; - } else { - quantityStringID = R.plurals.dvr_count_new_recordings; - } - } - mCardView.setContent(mCardView.getResources() - .getQuantityString(quantityStringID, count, count), null); - } - } - - public SeriesRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); - mDvrDataManager = singletons.getDvrDataManager(); - mDvrManager = singletons.getDvrManager(); - mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, - mWatchedPositionManager); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder; - final SeriesRecording seriesRecording = (SeriesRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.onBound(seriesRecording); - setTitleAndImage(cardView, seriesRecording); - super.onBindViewHolder(baseHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - ((RecordingCardView) viewHolder.view).reset(); - ((SeriesRecordingViewHolder) viewHolder).onUnbound(); - super.onUnbindViewHolder(viewHolder); - } - - private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { - cardView.setTitle(recording.getTitle()); - if (recording.getPosterUri() != null) { - cardView.setImageUri(recording.getPosterUri(), false); - } else { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - String imageUri = null; - if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - } - cardView.setImageUri(imageUri, true); - } - } -} diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java deleted file mode 100644 index 6c05c9c6..00000000 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ /dev/null @@ -1,397 +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.FragmentManager; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.util.Log; -import android.util.LongSparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ProgressBar; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.data.Program; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecording.ChannelOption; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Fragment for DVR series recording settings. - */ -public class SeriesSettingsFragment extends GuidedStepFragment - implements DvrDataManager.SeriesRecordingListener { - private static final String TAG = "SeriesSettingsFragment"; - private static final boolean DEBUG = false; - - private static final long ACTION_ID_PRIORITY = 10; - private static final long ACTION_ID_CHANNEL = 11; - - private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; - // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id - private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; - - private DvrDataManager mDvrDataManager; - private ChannelDataManager mChannelDataManager; - private DvrManager mDvrManager; - private SeriesRecording mSeriesRecording; - private long mSeriesRecordingId; - @ChannelOption int mChannelOption; - private Comparator mChannelComparator; - private long mSelectedChannelId; - private int mBackStackCount; - private boolean mShowViewScheduleOptionInDialog; - - private String mFragmentTitle; - private String mProrityActionTitle; - private String mProrityActionHighestText; - private String mProrityActionLowestText; - private String mChannelsActionTitle; - private String mChannelsActionAllText; - private LongSparseArray mId2Channel = new LongSparseArray<>(); - private List mChannels = new ArrayList<>(); - private EpisodicProgramLoadTask mEpisodicProgramLoadTask; - - private GuidedAction mPriorityGuidedAction; - private GuidedAction mChannelsGuidedAction; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mBackStackCount = getFragmentManager().getBackStackEntryCount(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); - mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); - if (mSeriesRecording == null) { - getActivity().finish(); - return; - } - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); - mShowViewScheduleOptionInDialog = getArguments().getBoolean( - DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); - mDvrDataManager.addSeriesRecordingListener(this); - long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - if (channelIds == null) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - collectChannelsInBackground(); - } else { - for (long channelId : channelIds) { - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - } - } - mChannelOption = mSeriesRecording.getChannelOption(); - mSelectedChannelId = Channel.INVALID_ID; - if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mSelectedChannelId = channel.getId(); - } else { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; - } - } - mChannelComparator = new Channel.DefaultComparator(context, - TvApplication.getSingletons(context).getTvInputManagerHelper()); - mChannels.sort(mChannelComparator); - mFragmentTitle = getString(R.string.dvr_series_settings_title); - mProrityActionTitle = getString(R.string.dvr_series_settings_priority); - mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); - mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest); - mChannelsActionTitle = getString(R.string.dvr_series_settings_channels); - mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all); - } - - @Override - public void onDetach() { - super.onDetach(); - mDvrDataManager.removeSeriesRecordingListener(this); - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } - } - - @Override - public void onDestroy() { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount - && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) - && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { - dvrManager.removeSeriesRecording(mSeriesRecordingId); - } - super.onDestroy(); - } - - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String breadcrumb = mSeriesRecording.getTitle(); - String title = mFragmentTitle; - return new Guidance(title, null, breadcrumb, null); - } - - @Override - public void onCreateActions(List actions, Bundle savedInstanceState) { - mPriorityGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_PRIORITY) - .title(mProrityActionTitle) - .build(); - updatePriorityGuidedAction(false); - actions.add(mPriorityGuidedAction); - - mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) - .id(ACTION_ID_CHANNEL) - .title(mChannelsActionTitle) - .subActions(buildChannelSubAction()) - .build(); - actions.add(mChannelsGuidedAction); - updateChannelsGuidedAction(false); - } - - @Override - public void onCreateButtonActions(List actions, Bundle savedInstanceState) { - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_OK) - .build()); - actions.add(new GuidedAction.Builder(getActivity()) - .clickAction(GuidedAction.ACTION_ID_CANCEL) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == GuidedAction.ACTION_ID_OK) { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } - if (mChannelOption != mSeriesRecording.getChannelOption() - || mSeriesRecording.isStopped() - || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE - && mSeriesRecording.getChannelId() != mSelectedChannelId)) { - SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) - .setChannelOption(mChannelOption) - .setState(SeriesRecording.STATE_SERIES_NORMAL); - if (mSelectedChannelId != Channel.INVALID_ID) { - builder.setChannelId(mSelectedChannelId); - } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(builder.build()); - SeriesRecordingScheduler scheduler = - SeriesRecordingScheduler.getInstance(getContext()); - // Since dialog is used even after the fragment is closed, we should - // use application context. - ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( - R.string.dvr_series_schedules_progress_message_updating_programs)); - scheduler.addOnSeriesRecordingUpdatedListener( - new OnSeriesRecordingUpdatedListener() { - @Override - public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - dialog.dismiss(); - scheduler.removeOnSeriesRecordingUpdatedListener(this); - showConfirmDialog(); - return; - } - } - } - }); - } else { - showConfirmDialog(); - } - } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { - finishGuidedStepFragments(); - } else if (actionId == ACTION_ID_PRIORITY) { - FragmentManager fragmentManager = getFragmentManager(); - PrioritySettingsFragment fragment = new PrioritySettingsFragment(); - Bundle args = new Bundle(); - args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, - mSeriesRecording.getId()); - fragment.setArguments(args); - GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); - } - } - - @Override - public boolean onSubGuidedActionClicked(GuidedAction action) { - long actionId = action.getId(); - if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; - mSelectedChannelId = Channel.INVALID_ID; - updateChannelsGuidedAction(true); - return true; - } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { - mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; - mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; - updateChannelsGuidedAction(true); - return true; - } - return false; - } - - @Override - public GuidedActionsStylist onCreateButtonActionsStylist() { - return new DvrGuidedActionsStylist(true); - } - - private void updateChannelsGuidedAction(boolean notifyActionChanged) { - if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { - mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else { - mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) - .getDisplayText()); - } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - } - } - - private void updatePriorityGuidedAction(boolean notifyActionChanged) { - int totalSeriesCount = 0; - int priorityOrder = 0; - for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { - if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL - || seriesRecording.getId() == mSeriesRecording.getId()) { - ++totalSeriesCount; - } - if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL - && seriesRecording.getId() != mSeriesRecording.getId() - && seriesRecording.getPriority() > mSeriesRecording.getPriority()) { - ++priorityOrder; - } - } - if (priorityOrder == 0) { - mPriorityGuidedAction.setDescription(mProrityActionHighestText); - } else if (priorityOrder >= totalSeriesCount - 1) { - mPriorityGuidedAction.setDescription(mProrityActionLowestText); - } else { - mPriorityGuidedAction.setDescription(getString( - R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); - } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); - } - } - - private void collectChannelsInBackground() { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - } - mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { - @Override - protected void onPostExecute(List programs) { - mEpisodicProgramLoadTask = null; - Set channelIds = new HashSet<>(); - for (Program program : programs) { - channelIds.add(program.getChannelId()); - } - boolean channelAdded = false; - for (Long channelId : channelIds) { - if (mId2Channel.get(channelId) != null) { - continue; - } - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - channelAdded = true; - mId2Channel.put(channelId, channel); - mChannels.add(channel); - if (DEBUG) Log.d(TAG, "Added channel: " + channel); - } - } - if (!channelAdded) { - return; - } - mChannels.sort(mChannelComparator); - mChannelsGuidedAction.setSubActions(buildChannelSubAction()); - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - mEpisodicProgramLoadTask.execute(); - } - - private List buildChannelSubAction() { - List channelSubActions = new ArrayList<>(); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ALL) - .title(mChannelsActionAllText) - .build()); - for (Channel channel : mChannels) { - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) - .title(channel.getDisplayText()) - .build()); - } - return channelSubActions; - } - - private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity( - getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); - finishGuidedStepFragments(); - } - - @Override - public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } - - @Override - public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - mSeriesRecording = seriesRecording; - updatePriorityGuidedAction(true); - return; - } - } - } -} diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 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 extends ArrayObjectAdapter { private final Comparator mComparator; private final int mMaxItemCount; private int mExtraItemCount; + private final Set mIds = new HashSet<>(); - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator) { + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator) { this(presenterSelector, comparator, Integer.MAX_VALUE); } - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator, + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; + setHasStableIds(true); } /** @@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter extends ArrayObjectAdapter { final void setInitialItems(List items) { List 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 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 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 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/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java new file mode 100644 index 00000000..38a78f5d --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -0,0 +1,134 @@ +/* + * 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.graphics.drawable.Drawable; +import android.support.v17.leanback.R; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.PresenterSelector; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +// This class is adapted from Leanback's library, which does not support action icon with one-line +// label. This class modified its getPresenter method to support the above situation. +class ActionPresenterSelector extends PresenterSelector { + private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); + private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); + private final Presenter[] mPresenters = new Presenter[] { + mOneLineActionPresenter, mTwoLineActionPresenter}; + + @Override + public Presenter getPresenter(Object item) { + Action action = (Action) item; + if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) { + return mOneLineActionPresenter; + } else { + return mTwoLineActionPresenter; + } + } + + @Override + public Presenter[] getPresenters() { + return mPresenters; + } + + static class ActionViewHolder extends Presenter.ViewHolder { + Action mAction; + Button mButton; + int mLayoutDirection; + + public ActionViewHolder(View view, int layoutDirection) { + super(view); + mButton = (Button) view.findViewById(R.id.lb_action_button); + mLayoutDirection = layoutDirection; + } + } + + class OneLineActionPresenter extends Presenter { + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_1_line, parent, false); + return new ActionViewHolder(v, parent.getLayoutDirection()); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + Action action = (Action) item; + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mAction = action; + vh.mButton.setText(action.getLabel1()); + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + ((ActionViewHolder) viewHolder).mAction = null; + } + } + + class TwoLineActionPresenter extends Presenter { + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.lb_action_2_lines, parent, false); + return new ActionViewHolder(v, parent.getLayoutDirection()); + } + + @Override + public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + Action action = (Action) item; + ActionViewHolder vh = (ActionViewHolder) viewHolder; + Drawable icon = action.getIcon(); + vh.mAction = action; + + if (icon != null) { + final int startPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start); + final int endPadding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end); + vh.view.setPaddingRelative(startPadding, 0, endPadding, 0); + } else { + final int padding = vh.view.getResources() + .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); + vh.view.setPaddingRelative(padding, 0, padding, 0); + } + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); + + CharSequence line1 = action.getLabel1(); + CharSequence line2 = action.getLabel2(); + if (TextUtils.isEmpty(line1)) { + vh.mButton.setText(line2); + } else if (TextUtils.isEmpty(line2)) { + vh.mButton.setText(line1); + } else { + vh.mButton.setText(line1 + "\n" + line2); + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { + ActionViewHolder vh = (ActionViewHolder) viewHolder; + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); + vh.view.setPadding(0, 0, 0, 0); + vh.mAction = null; + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/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/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java new file mode 100644 index 00000000..b43d1f12 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.media.tv.TvContract; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; + +/** + * A class for details content. + */ +class DetailsContent { + /** Constant for invalid time. */ + public static final long INVALID_TIME = -1; + + private CharSequence mTitle; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private String mDescription; + private String mLogoImageUri; + private String mBackgroundImageUri; + + private DetailsContent() { } + + /** + * Returns title. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns start time. + */ + public long getStartTimeUtcMillis() { + return mStartTimeUtcMillis; + } + + /** + * Returns end time. + */ + public long getEndTimeUtcMillis() { + return mEndTimeUtcMillis; + } + + /** + * Returns description. + */ + public String getDescription() { + return mDescription; + } + + /** + * Returns Logo image URI as a String. + */ + public String getLogoImageUri() { + return mLogoImageUri; + } + + /** + * Returns background image URI as a String. + */ + public String getBackgroundImageUri() { + return mBackgroundImageUri; + } + + /** + * Copies other details content. + */ + public void copyFrom(DetailsContent other) { + if (this == other) { + return; + } + mTitle = other.mTitle; + mStartTimeUtcMillis = other.mStartTimeUtcMillis; + mEndTimeUtcMillis = other.mEndTimeUtcMillis; + mDescription = other.mDescription; + mLogoImageUri = other.mLogoImageUri; + mBackgroundImageUri = other.mBackgroundImageUri; + } + + /** + * A class for building details content. + */ + public static final class Builder { + private final DetailsContent mDetailsContent; + + public Builder() { + mDetailsContent = new DetailsContent(); + mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; + mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; + } + + /** + * Sets title. + */ + public Builder setTitle(CharSequence title) { + mDetailsContent.mTitle = title; + return this; + } + + /** + * Sets start time. + */ + public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { + mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; + return this; + } + + /** + * Sets end time. + */ + public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { + mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; + return this; + } + + /** + * Sets description. + */ + public Builder setDescription(String description) { + mDetailsContent.mDescription = description; + return this; + } + + /** + * Sets logo image URI as a String. + */ + public Builder setLogoImageUri(String logoImageUri) { + mDetailsContent.mLogoImageUri = logoImageUri; + return this; + } + + /** + * Sets background image URI as a String. + */ + public Builder setBackgroundImageUri(String backgroundImageUri) { + mDetailsContent.mBackgroundImageUri = backgroundImageUri; + return this; + } + + /** + * Sets background image and logo image URI from program and channel. + */ + public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) { + if (program != null) { + return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel); + } else { + return setImageUris(null, null, channel); + } + } + + /** + * Sets background image and logo image URI and channel is used for fallback images. + */ + public Builder setImageUris(@Nullable String posterArtUri, + @Nullable String thumbnailUri, @Nullable Channel channel) { + mDetailsContent.mLogoImageUri = null; + mDetailsContent.mBackgroundImageUri = null; + if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) { + mDetailsContent.mLogoImageUri = posterArtUri; + mDetailsContent.mBackgroundImageUri = thumbnailUri; + } else if (!TextUtils.isEmpty(posterArtUri)) { + // thumbnailUri is empty + mDetailsContent.mLogoImageUri = posterArtUri; + mDetailsContent.mBackgroundImageUri = posterArtUri; + } else if (!TextUtils.isEmpty(thumbnailUri)) { + // posterArtUri is empty + mDetailsContent.mLogoImageUri = thumbnailUri; + mDetailsContent.mBackgroundImageUri = thumbnailUri; + } + if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { + String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) + .toString(); + mDetailsContent.mLogoImageUri = channelLogoUri; + mDetailsContent.mBackgroundImageUri = channelLogoUri; + } + return this; + } + + /** + * Builds details content. + */ + public DetailsContent build() { + DetailsContent detailsContent = new DetailsContent(); + detailsContent.copyFrom(mDetailsContent); + return detailsContent; + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java new file mode 100644 index 00000000..a2e3fe16 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -0,0 +1,299 @@ +/* + * 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.app.Activity; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.support.v17.leanback.widget.Presenter; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.tv.R; +import com.android.tv.ui.ViewUtils; +import com.android.tv.util.Utils; + +/** + * An {@link Presenter} for rendering a detailed description of an DVR item. + * 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}. + */ +class DetailsContentPresenter extends Presenter { + /** + * The ViewHolder for the {@link DetailsContentPresenter}. + */ + public static class ViewHolder extends Presenter.ViewHolder { + final TextView mTitle; + final TextView mSubtitle; + final LinearLayout mDescriptionContainer; + final TextView mBody; + final TextView mReadMoreView; + final int mTitleMargin; + final int mUnderTitleBaselineMargin; + final int mUnderSubtitleBaselineMargin; + final int mTitleLineSpacing; + final int mBodyLineSpacing; + final int mBodyMaxLines; + final int mBodyMinLines; + final FontMetricsInt mTitleFontMetricsInt; + final FontMetricsInt mSubtitleFontMetricsInt; + final FontMetricsInt mBodyFontMetricsInt; + final int mTitleMaxLines; + + private Activity mActivity; + private boolean mFullTextMode; + private int mFullTextAnimationDuration; + private boolean mIsListeningToPreDraw; + + private ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (mSubtitle.getVisibility() == View.VISIBLE + && mSubtitle.getTop() > view.getHeight() + && mTitle.getLineCount() > 1) { + mTitle.setMaxLines(mTitle.getLineCount() - 1); + return false; + } + final int bodyLines = mBody.getLineCount(); + final int maxLines = mFullTextMode ? bodyLines : + (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); + if (bodyLines > maxLines) { + mReadMoreView.setVisibility(View.VISIBLE); + mDescriptionContainer.setFocusable(true); + mDescriptionContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mFullTextMode = true; + mReadMoreView.setVisibility(View.GONE); + mDescriptionContainer.setFocusable(false); + mDescriptionContainer.setOnClickListener(null); + mBody.setMaxLines(bodyLines); + // Minus 1 from line difference to eliminate the space + // originally occupied by "READ MORE" + showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); + } + }); + } + if (mBody.getMaxLines() != maxLines) { + mBody.setMaxLines(maxLines); + return false; + } else { + removePreDrawListener(); + return true; + } + } + }; + + public ViewHolder(final View view) { + super(view); + 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); + mDescriptionContainer = + (LinearLayout) view.findViewById(R.id.dvr_details_description_container); + mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); + + FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); + final int titleAscent = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_baseline); + // Ascent is negative + mTitleMargin = titleAscent + titleFontMetricsInt.ascent; + + mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_title_baseline_margin); + mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_subtitle_baseline_margin); + + mTitleLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_line_spacing); + mBodyLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_body_line_spacing); + + mBodyMaxLines = view.getResources().getInteger( + R.integer.lb_details_description_body_max_lines); + mBodyMinLines = view.getResources().getInteger( + R.integer.lb_details_description_body_min_lines); + mTitleMaxLines = mTitle.getMaxLines(); + + mTitleFontMetricsInt = getFontMetricsInt(mTitle); + mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); + mBodyFontMetricsInt = getFontMetricsInt(mBody); + } + + void addPreDrawListener() { + if (!mIsListeningToPreDraw) { + mIsListeningToPreDraw = true; + view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + } + + void removePreDrawListener() { + if (mIsListeningToPreDraw) { + view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); + mIsListeningToPreDraw = false; + } + } + + public TextView getTitle() { + return mTitle; + } + + public TextView getSubtitle() { + return mSubtitle; + } + + public TextView getBody() { + return mBody; + } + + private FontMetricsInt getFontMetricsInt(TextView textView) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setTextSize(textView.getTextSize()); + paint.setTypeface(textView.getTypeface()); + return paint.getFontMetricsInt(); + } + + private void showFullText(int heightDiff) { + final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); + int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); + Animator expandAnimator = ViewUtils.createHeightAnimator( + detailsFrame, nowHeight, nowHeight + heightDiff); + expandAnimator.setDuration(mFullTextAnimationDuration); + Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + 0f, -(heightDiff / 2))); + shiftAnimator.setDuration(mFullTextAnimationDuration); + AnimatorSet fullTextAnimator = new AnimatorSet(); + fullTextAnimator.playTogether(expandAnimator, shiftAnimator); + fullTextAnimator.start(); + } + } + + private final Activity mActivity; + private final int mFullTextAnimationDuration; + + public DetailsContentPresenter(Activity activity) { + super(); + mActivity = activity; + mFullTextAnimationDuration = mActivity.getResources() + .getInteger(R.integer.dvr_details_full_text_animation_duration); + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.dvr_details_description, parent, false); + return new ViewHolder(v); + } + + @Override + public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + final ViewHolder vh = (ViewHolder) viewHolder; + final DetailsContent detailsContent = (DetailsContent) item; + + vh.mActivity = mActivity; + vh.mFullTextAnimationDuration = mFullTextAnimationDuration; + + boolean hasTitle = true; + if (TextUtils.isEmpty(detailsContent.getTitle())) { + vh.mTitle.setVisibility(View.GONE); + hasTitle = false; + } else { + vh.mTitle.setText(detailsContent.getTitle()); + vh.mTitle.setVisibility(View.VISIBLE); + vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() + + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); + vh.mTitle.setMaxLines(vh.mTitleMaxLines); + } + setTopMargin(vh.mTitle, vh.mTitleMargin); + + boolean hasSubtitle = true; + if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME + && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { + vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), + detailsContent.getStartTimeUtcMillis(), + detailsContent.getEndTimeUtcMillis(), false)); + vh.mSubtitle.setVisibility(View.VISIBLE); + if (hasTitle) { + setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin + + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); + } else { + setTopMargin(vh.mSubtitle, 0); + } + } else { + vh.mSubtitle.setVisibility(View.GONE); + hasSubtitle = false; + } + + if (TextUtils.isEmpty(detailsContent.getDescription())) { + vh.mBody.setVisibility(View.GONE); + } else { + vh.mBody.setText(detailsContent.getDescription()); + vh.mBody.setVisibility(View.VISIBLE); + vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() + + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); + if (hasSubtitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else if (hasTitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else { + setTopMargin(vh.mDescriptionContainer, 0); + } + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } + + private void setTopMargin(View view, int topMargin) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + lp.topMargin = topMargin; + view.setLayoutParams(lp); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java new file mode 100644 index 00000000..82fe9ce3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -0,0 +1,92 @@ +/* + * 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.app.Activity; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.support.v17.leanback.app.BackgroundManager; + +/** + * The Background Helper. + */ +class DetailsViewBackgroundHelper { + // Background delay serves to avoid kicking off expensive bitmap loading + // in case multiple backgrounds are set in quick succession. + private static final int SET_BACKGROUND_DELAY_MS = 100; + + private final BackgroundManager mBackgroundManager; + + class LoadBackgroundRunnable implements Runnable { + final Drawable mBackGround; + + LoadBackgroundRunnable(Drawable background) { + mBackGround = background; + } + + @Override + public void run() { + if (!mBackgroundManager.isAttached()) { + return; + } + if (mBackGround instanceof BitmapDrawable) { + mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap()); + } + mRunnable = null; + } + } + + private LoadBackgroundRunnable mRunnable; + + private final Handler mHandler = new Handler(); + + public DetailsViewBackgroundHelper(Activity activity) { + mBackgroundManager = BackgroundManager.getInstance(activity); + mBackgroundManager.attach(activity.getWindow()); + } + + /** + * Sets the given image to background. + */ + public void setBackground(Drawable background) { + if (mRunnable != null) { + mHandler.removeCallbacks(mRunnable); + } + mRunnable = new LoadBackgroundRunnable(background); + mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS); + } + + /** + * Sets the background color. + */ + public void setBackgroundColor(int color) { + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setColor(color); + } + } + + /** + * Sets the background scrim. + */ + public void setScrim(int color) { + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setDimLayer(new ColorDrawable(color)); + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java new file mode 100644 index 00000000..2b3dcb25 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.app.Activity; +import android.os.Bundle; + +import com.android.tv.R; +import com.android.tv.TvApplication; + +/** + * {@link android.app.Activity} for DVR UI. + */ +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/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java new file mode 100644 index 00000000..803d1017 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.support.v17.leanback.app.BrowseFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.TitleViewAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.GenreItems; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +import com.android.tv.dvr.DvrScheduleManager; +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; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; + +/** + * {@link BrowseFragment} for DVR functions. + */ +public class DvrBrowseFragment extends BrowseFragment implements + RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener, + OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener { + private static final String TAG = "DvrBrowseFragment"; + private static final boolean DEBUG = false; + + private static final int MAX_RECENT_ITEM_COUNT = 10; + private static final int MAX_SCHEDULED_ITEM_COUNT = 4; + + private RecordedProgramAdapter mRecentAdapter; + private ScheduleAdapter mScheduleAdapter; + private SeriesAdapter mSeriesAdapter; + private RecordedProgramAdapter[] mGenreAdapters = + new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; + private ListRow mRecentRow; + private ListRow mSeriesRow; + private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; + private List mGenreLabels; + private DvrDataManager mDvrDataManager; + private DvrScheduleManager mDvrScheudleManager; + private ArrayObjectAdapter mRowsAdapter; + private ClassPresenterSelector mPresenterSelector; + private final HashMap 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 RECORDED_PROGRAM_COMPARATOR = new Comparator() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof SeriesRecording) { + lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); + } + if (rhs instanceof SeriesRecording) { + rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); + } + if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else { + return -1; + } + } else if (rhs instanceof RecordedProgram) { + return 1; + } else { + return 0; + } + } + }; + + private final Comparator SCHEDULE_COMPARATOR = new Comparator() { + @Override + public int compare(Object lhs, Object rhs) { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else { + return -1; + } + } else if (rhs instanceof ScheduledRecording) { + return 1; + } else { + return 0; + } + } + }; + + private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = + new DvrScheduleManager.OnConflictStateChangeListener() { + @Override + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { + if (mScheduleAdapter != null) { + for (ScheduledRecording schedule : schedules) { + onScheduledRecordingConflictStatusChanged(schedule); + } + } + } + }; + + private final Runnable mUpdateRowsRunnable = new Runnable() { + @Override + public void run() { + updateRows(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + Context context = getContext(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrScheudleManager = singletons.getDvrScheduleManager(); + mPresenterSelector = new ClassPresenterSelector() + .addClassPresenter(ScheduledRecording.class, + new ScheduledRecordingPresenter(context)) + .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) + .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) + .addClassPresenter(FullScheduleCardHolder.class, + new FullSchedulesCardPresenter(context)); + mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); + mGenreLabels.add(getString(R.string.dvr_main_others)); + prepareUiElements(); + if (!startBrowseIfDvrInitialized()) { + if (!mDvrDataManager.isDvrScheduleLoadFinished()) { + mDvrDataManager.addDvrScheduleLoadFinishedListener(this); + } + if (!mDvrDataManager.isRecordedProgramLoadFinished()) { + mDvrDataManager.addRecordedProgramLoadFinishedListener(this); + } + } + } + + @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); + mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); + mDvrDataManager.removeRecordedProgramListener(this); + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrDataManager.removeSeriesRecordingListener(this); + mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + mRowsAdapter.clear(); + mSeriesId2LatestProgram.clear(); + for (Presenter presenter : mPresenterSelector.getPresenters()) { + if (presenter instanceof DvrItemPresenter) { + ((DvrItemPresenter) presenter).unbindAllViewHolders(); + } + } + super.onDestroy(); + } + + @Override + public void onDvrScheduleLoadFinished() { + startBrowseIfDvrInitialized(); + mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); + } + + @Override + public void onRecordedProgramLoadFinished() { + startBrowseIfDvrInitialized(); + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramAdded(recordedProgram, true); + } + postUpdateRows(); + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramChanged(recordedProgram); + } + postUpdateRows(); + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + handleRecordedProgramRemoved(recordedProgram); + } + postUpdateRows(); + } + + // No need to call updateRows() during ScheduledRecordings' change because + // the row for ScheduledRecordings is always displayed. + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + if (needToShowScheduledRecording(scheduleRecording)) { + mScheduleAdapter.add(scheduleRecording); + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + mScheduleAdapter.remove(scheduleRecording); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduleRecording : scheduledRecordings) { + if (needToShowScheduledRecording(scheduleRecording)) { + mScheduleAdapter.change(scheduleRecording); + } else { + mScheduleAdapter.removeWithId(scheduleRecording); + } + } + } + + 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)); + postUpdateRows(); + } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings)); + postUpdateRows(); + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings)); + postUpdateRows(); + } + + // Workaround of b/29108300 + @Override + public void showTitle(int flags) { + flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE; + super.showTitle(flags); + } + + private void 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 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 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 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, + boolean updateSeriesRecording) { + mRecentAdapter.add(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + SeriesRecording seriesRecording = null; + if (seriesId != null) { + seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); + if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .compare(latestProgram, recordedProgram) < 0) { + mSeriesId2LatestProgram.put(seriesId, recordedProgram); + if (updateSeriesRecording && seriesRecording != null) { + onSeriesRecordingChanged(seriesRecording); + } + } + } + if (seriesRecording == null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + adapter.add(recordedProgram); + } + } + } + + private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) { + mRecentAdapter.remove(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + if (seriesId != null) { + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = + mSeriesId2LatestProgram.get(recordedProgram.getSeriesId()); + if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) { + if (seriesRecording != null) { + updateLatestRecordedProgram(seriesRecording); + onSeriesRecordingChanged(seriesRecording); + } + } + } + for (RecordedProgramAdapter adapter + : getGenreAdapters(recordedProgram.getCanonicalGenres())) { + adapter.remove(recordedProgram); + } + } + + private void handleRecordedProgramChanged(RecordedProgram recordedProgram) { + mRecentAdapter.change(recordedProgram); + String seriesId = recordedProgram.getSeriesId(); + SeriesRecording seriesRecording = null; + if (seriesId != null) { + seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId); + if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .compare(latestProgram, recordedProgram) <= 0) { + mSeriesId2LatestProgram.put(seriesId, recordedProgram); + if (seriesRecording != null) { + onSeriesRecordingChanged(seriesRecording); + } + } else if (latestProgram.getId() == recordedProgram.getId()) { + if (seriesRecording != null) { + updateLatestRecordedProgram(seriesRecording); + onSeriesRecordingChanged(seriesRecording); + } + } + } + if (seriesRecording == null) { + updateGenreAdapters(getGenreAdapters( + recordedProgram.getCanonicalGenres()), recordedProgram); + } else { + updateGenreAdapters(new ArrayList<>(), recordedProgram); + } + } + + private void handleSeriesRecordingsAdded(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.add(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.add(seriesRecording); + } + } + } + } + + private void handleSeriesRecordingsRemoved(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.remove(seriesRecording); + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.remove(seriesRecording); + } + } + } + + private void handleSeriesRecordingsChanged(List seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + mSeriesAdapter.change(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + updateGenreAdapters(getGenreAdapters( + seriesRecording.getCanonicalGenreIds()), seriesRecording); + } else { + // Remove series recording from all genre rows if it has no recorded program + updateGenreAdapters(new ArrayList<>(), seriesRecording); + } + } + } + + private List getGenreAdapters(String[] genres) { + List result = new ArrayList<>(); + if (genres == null || genres.length == 0) { + result.add(mGenreAdapters[mGenreAdapters.length - 1]); + } else { + for (String genre : genres) { + int genreId = GenreItems.getId(genre); + if(genreId >= mGenreAdapters.length) { + Log.d(TAG, "Wrong Genre ID: " + genreId); + } else { + result.add(mGenreAdapters[genreId]); + } + } + } + return result; + } + + private List getGenreAdapters(int[] genreIds) { + List result = new ArrayList<>(); + if (genreIds == null || genreIds.length == 0) { + result.add(mGenreAdapters[mGenreAdapters.length - 1]); + } else { + for (int genreId : genreIds) { + if(genreId >= mGenreAdapters.length) { + Log.d(TAG, "Wrong Genre ID: " + genreId); + } else { + result.add(mGenreAdapters[genreId]); + } + } + } + return result; + } + + private void updateGenreAdapters(List adapters, Object r) { + for (RecordedProgramAdapter adapter : mGenreAdapters) { + if (adapters.contains(adapter)) { + adapter.change(r); + } else { + adapter.remove(r); + } + } + } + + private void postUpdateRows() { + mHandler.removeCallbacks(mUpdateRowsRunnable); + mHandler.post(mUpdateRowsRunnable); + } + + private void updateRows() { + int visibleRowsCount = 1; // Schedule's Row will never be empty + if (mRecentAdapter.isEmpty()) { + mRowsAdapter.remove(mRecentRow); + } else { + if (mRowsAdapter.indexOf(mRecentRow) < 0) { + mRowsAdapter.add(0, mRecentRow); + } + visibleRowsCount++; + } + if (mSeriesAdapter.isEmpty()) { + mRowsAdapter.remove(mSeriesRow); + } else { + if (mRowsAdapter.indexOf(mSeriesRow) < 0) { + mRowsAdapter.add(visibleRowsCount, mSeriesRow); + } + visibleRowsCount++; + } + for (int i = 0; i < mGenreAdapters.length; i++) { + RecordedProgramAdapter adapter = mGenreAdapters[i]; + if (adapter != null) { + if (adapter.isEmpty()) { + mRowsAdapter.remove(mGenreRows[i]); + } else { + if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) { + mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter); + mRowsAdapter.add(visibleRowsCount, mGenreRows[i]); + } + visibleRowsCount++; + } + } + } + } + + private boolean needToShowScheduledRecording(ScheduledRecording recording) { + int state = recording.getState(); + return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { + RecordedProgram latestProgram = null; + for (RecordedProgram program : + mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { + if (latestProgram == null || RecordedProgram + .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { + latestProgram = program; + } + } + mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram); + } + + private class ScheduleAdapter extends SortedArrayAdapter { + ScheduleAdapter(int maxItemCount) { + super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount); + } + + @Override + public long getId(Object item) { + if (item instanceof ScheduledRecording) { + return ((ScheduledRecording) item).getId(); + } else { + return -1; + } + } + } + + private class SeriesAdapter extends SortedArrayAdapter { + SeriesAdapter() { + super(mPresenterSelector, new Comparator() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; + } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + } + }); + } + + @Override + public long getId(SeriesRecording item) { + return item.getId(); + } + } + + private class RecordedProgramAdapter extends SortedArrayAdapter { + RecordedProgramAdapter() { + this(Integer.MAX_VALUE); + } + + RecordedProgramAdapter(int maxItemCount) { + super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount); + } + + @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() - 1; + } else { + return -1; + } + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java new file mode 100644 index 00000000..30c81e83 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java @@ -0,0 +1,98 @@ +/* + * 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.app.Activity; +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; + +import com.android.tv.R; +import com.android.tv.TvApplication; + +/** + * Activity to show details view in DVR. + */ +public class DvrDetailsActivity extends Activity { + /** + * Name of record id added to the Intent. + */ + public static final String RECORDING_ID = "record_id"; + + /** + * Name of flag added to the Intent to determine if details view should hide "View schedule" + * button. + */ + public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; + + /** + * Name of details view's type added to the intent. + */ + public static final String DETAILS_VIEW_TYPE = "details_view_type"; + + /** + * Name of shared element between activities. + */ + public static final String SHARED_ELEMENT_NAME = "shared_element"; + + /** + * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. + */ + public static final int CURRENT_RECORDING_VIEW = 1; + + /** + * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. + */ + public static final int SCHEDULED_RECORDING_VIEW = 2; + + /** + * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. + */ + public static final int RECORDED_PROGRAM_VIEW = 3; + + /** + * SERIES_RECORDING_VIEW refers to series recording in DVR. + */ + public static final int SERIES_RECORDING_VIEW = 4; + + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_details); + long recordId = getIntent().getLongExtra(RECORDING_ID, -1); + int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); + boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); + if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { + Bundle args = new Bundle(); + args.putLong(RECORDING_ID, recordId); + DetailsFragment detailsFragment = null; + if (detailsViewType == CURRENT_RECORDING_VIEW) { + detailsFragment = new CurrentRecordingDetailsFragment(); + } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { + args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); + detailsFragment = new ScheduledRecordingDetailsFragment(); + } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { + detailsFragment = new RecordedProgramDetailsFragment(); + } else if (detailsViewType == SERIES_RECORDING_VIEW) { + detailsFragment = new SeriesRecordingDetailsFragment(); + } + detailsFragment.setArguments(args); + getFragmentManager().beginTransaction() + .replace(R.id.dvr_details_view_frame, detailsFragment).commit(); + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java new file mode 100644 index 00000000..4d3698ef --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -0,0 +1,344 @@ +/* + * 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.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.DetailsOverviewRow; +import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PresenterSelector; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.support.v17.leanback.widget.VerticalGridView; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.widget.Toast; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.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; +import com.android.tv.util.Utils; + +import java.io.File; + +abstract class DvrDetailsFragment extends DetailsFragment { + private static final int LOAD_LOGO_IMAGE = 1; + private static final int LOAD_BACKGROUND_IMAGE = 2; + + protected DetailsViewBackgroundHelper mBackgroundHelper; + private ArrayObjectAdapter mRowsAdapter; + private DetailsOverviewRow mDetailsOverview; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (!onLoadRecordingDetails(getArguments())) { + getActivity().finish(); + return; + } + mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); + setupAdapter(); + onCreateInternal(); + } + + @Override + public void onStart() { + super.onStart(); + // TODO: remove the workaround of b/30401180. + VerticalGridView container = (VerticalGridView) getActivity() + .findViewById(R.id.container_list); + // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. + container.setItemAlignmentOffset(0); + container.setWindowAlignmentOffset( + getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); + } + + private void setupAdapter() { + DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( + new DetailsContentPresenter(getActivity())); + rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, + null)); + rowPresenter.setSharedElementEnterTransition(getActivity(), + DvrDetailsActivity.SHARED_ELEMENT_NAME); + rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); + mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); + setAdapter(mRowsAdapter); + } + + /** + * Returns details views' rows adapter. + */ + protected ArrayObjectAdapter getRowsAdapter() { + return mRowsAdapter; + } + + /** + * Sets details overview. + */ + protected void setDetailsOverviewRow(DetailsContent detailsContent) { + mDetailsOverview = new DetailsOverviewRow(detailsContent); + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + mRowsAdapter.add(mDetailsOverview); + onLoadLogoAndBackgroundImages(detailsContent); + } + + /** + * Creates and returns presenter selector will be used by rows adaptor. + */ + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); + return presenterSelector; + } + + /** + * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish + * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not + * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to + * do after the super class did onCreate, it should override this method and put the codes here. + */ + protected void onCreateInternal() { } + + /** + * Updates actions of details overview. + */ + protected void updateActions() { + mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); + } + + /** + * Loads recording details according to the arguments the fragment got. + * + * @return false if cannot find valid recordings, else return true. If the return value + * is false, the detail activity and fragment will be ended. + */ + abstract boolean onLoadRecordingDetails(Bundle args); + + /** + * Creates actions users can interact with and their adaptor for this fragment. + */ + abstract SparseArrayObjectAdapter onCreateActionsAdapter(); + + /** + * Creates actions listeners to implement the behavior of the fragment after users click some + * action buttons. + */ + abstract OnActionClickedListener onCreateOnActionClickedListener(); + + /** + * Returns program title with episode number. If the program is null, returns channel name. + */ + protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { + String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); + SpannableString title = titleWithEpisodeNumber == null ? null + : new SpannableString(titleWithEpisodeNumber); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : getContext().getResources().getString( + R.string.no_program_information)); + } else { + String programTitle = program.getTitle(); + title.setSpan(new TextAppearanceSpan(getContext(), + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return title; + } + + /** + * Loads logo and background images for detail fragments. + */ + protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { + Drawable logoDrawable = null; + Drawable backgroundDrawable = null; + if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { + logoDrawable = getContext().getResources() + .getDrawable(R.drawable.dvr_default_poster, null); + mDetailsOverview.setImageDrawable(logoDrawable); + } + if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { + backgroundDrawable = getContext().getResources() + .getDrawable(R.drawable.dvr_default_poster, null); + mBackgroundHelper.setBackground(backgroundDrawable); + } + if (logoDrawable != null && backgroundDrawable != null) { + return; + } + if (logoDrawable == null && backgroundDrawable == null + && detailsContent.getLogoImageUri().equals( + detailsContent.getBackgroundImageUri())) { + ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), + new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, + getContext())); + return; + } + if (logoDrawable == null) { + int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); + int imageHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_details_poster_height); + ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), + imageWidth, imageHeight, + new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); + } + if (backgroundDrawable == null) { + ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), + new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); + } + } + + protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { + if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && + !isDataUriAccessible(recordedProgram.getDataUri())) { + // Since cleaning RecordedProgram from forgotten storage will take some time, + // ignore playback until cleaning is finished. + ToastUtils.show(getContext(), + getContext().getResources().getString(R.string.dvr_toast_recording_deleted), + Toast.LENGTH_SHORT); + return; + } + ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) + .getTvInputManagerHelper().getParentalControlSettings(); + if (!parental.isParentalControlsEnabled()) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); + if (channel != null && channel.isLocked()) { + checkPinToPlay(recordedProgram, seekTimeMs); + return; + } + String ratingString = recordedProgram.getContentRating(); + if (TextUtils.isEmpty(ratingString)) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + String[] ratingList = ratingString.split(","); + TvContentRating[] programRatings = new TvContentRating[ratingList.length]; + for (int i = 0; i < ratingList.length; i++) { + programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); + } + TvContentRating blockRatings = parental.getBlockedRating(programRatings); + if (blockRatings != null) { + checkPinToPlay(recordedProgram, seekTimeMs); + } else { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + } + } + + private boolean isDataUriAccessible(Uri dataUri) { + if (dataUri == null || dataUri.getPath() == null) { + return false; + } + try { + File recordedProgramPath = new File(dataUri.getPath()); + if (recordedProgramPath.exists()) { + return true; + } + } catch (SecurityException e) { + } + return false; + } + + private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + launchPlaybackActivity(recordedProgram, seekTimeMs, true); + } + } + }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + + private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, + boolean pinChecked) { + Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); + if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); + getActivity().startActivity(intent); + } + + private static class MyImageLoaderCallback extends + ImageLoader.ImageLoaderCallback { + private final Context mContext; + private final int mLoadType; + + public MyImageLoaderCallback(DvrDetailsFragment fragment, + int loadType, Context context) { + super(fragment); + mLoadType = loadType; + mContext = context; + } + + @Override + public void onBitmapLoaded(DvrDetailsFragment fragment, + @Nullable Bitmap bitmap) { + Drawable drawable; + int loadType = mLoadType; + if (bitmap == null) { + Resources res = mContext.getResources(); + drawable = res.getDrawable(R.drawable.dvr_default_poster, null); + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { + loadType &= ~LOAD_BACKGROUND_IMAGE; + fragment.mBackgroundHelper.setBackgroundColor( + res.getColor(R.color.dvr_detail_default_background)); + fragment.mBackgroundHelper.setScrim( + res.getColor(R.color.dvr_detail_default_background_scrim)); + } + } else { + drawable = new BitmapDrawable(mContext.getResources(), bitmap); + } + if (!fragment.isDetached()) { + if ((loadType & LOAD_LOGO_IMAGE) != 0) { + fragment.mDetailsOverview.setImageDrawable(drawable); + } + if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { + fragment.mBackgroundHelper.setBackground(drawable); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java new file mode 100644 index 00000000..317b6af3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java @@ -0,0 +1,83 @@ +/* + * 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.app.Activity; +import android.support.annotation.CallSuper; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.View.OnClickListener; + +import com.android.tv.dvr.ui.DvrUiHelper; + +import java.util.HashSet; +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 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 mBoundViewHolders = new HashSet<>(); + private final OnClickListener mOnClickListener = onCreateOnClickListener(); + + @Override + @CallSuper + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + viewHolder.view.setTag(o); + viewHolder.view.setOnClickListener(mOnClickListener); + mBoundViewHolders.add(viewHolder); + } + + @Override + @CallSuper + public void onUnbindViewHolder(ViewHolder viewHolder) { + mBoundViewHolders.remove(viewHolder); + viewHolder.view.setTag(null); + viewHolder.view.setOnClickListener(null); + } + + /** + * Unbinds all bound view holders. + */ + public void unbindAllViewHolders() { + // When browse fragments are destroyed, RecyclerView would not call presenters' + // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. + for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { + onUnbindViewHolder(viewHolder); + } + } + + /** + * Creates {@link OnClickListener} for DVR library's card views. + */ + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof RecordingCardView) { + RecordingCardView v = (RecordingCardView) view; + DvrUiHelper.startDetailsActivity((Activity) v.getContext(), + v.getTag(), v.getImageView(), false); + } + } + }; + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/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/browse/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java new file mode 100644 index 00000000..311137a9 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * Special object for schedule preview; + */ +final class FullScheduleCardHolder { + /** + * Full schedule card holder. + */ + static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder(); + + private FullScheduleCardHolder() { } +} diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java new file mode 100644 index 00000000..6d4763d4 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.Context; +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.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.util.Utils; + +import java.util.Collections; +import java.util.List; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +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 ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder vh, Object o) { + final RecordingCardView cardView = (RecordingCardView) vh.view; + + cardView.setImage(mIconDrawable); + cardView.setTitle(mCardTitleText); + List scheduledRecordings = TvApplication.getSingletons(mContext) + .getDvrDataManager().getAvailableScheduledRecordings(); + int fullDays = 0; + if (!scheduledRecordings.isEmpty()) { + fullDays = Utils.computeDateDifference(System.currentTimeMillis(), + Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) + .getStartTimeMs()) + 1; + } + cardView.setContent(mContext.getResources().getQuantityString( + R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); + super.onBindViewHolder(vh, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder vh) { + ((RecordingCardView) vh.view).reset(); + super.onUnbindViewHolder(vh); + } + + @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/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java new file mode 100644 index 00000000..fe9b9de5 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -0,0 +1,170 @@ +/* + * 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.res.Resources; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; + +/** + * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. + */ +public class RecordedProgramDetailsFragment extends DvrDetailsFragment + implements DvrDataManager.RecordedProgramListener { + private static final int ACTION_RESUME_PLAYING = 1; + private static final int ACTION_PLAY_FROM_BEGINNING = 2; + private static final int ACTION_DELETE_RECORDING = 3; + + private DvrWatchedPositionManager mDvrWatchedPositionManager; + + private RecordedProgram mRecordedProgram; + private DetailsContent mDetailsContent; + private boolean mPaused; + private DvrDataManager mDvrDataManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + mDvrDataManager.addRecordedProgramListener(this); + super.onCreate(savedInstanceState); + } + + @Override + public void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); + setDetailsOverviewRow(mDetailsContent); + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateActions(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + @Override + public void onDestroy() { + mDvrDataManager.removeRecordedProgramListener(this); + super.onDestroy(); + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); + if (mRecordedProgram == null) { + // notify super class to end activity before initializing anything + return false; + } + mDetailsContent = createDetailsContent(); + return true; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mRecordedProgram.getChannelId()); + String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription()) + ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription(); + return new DetailsContent.Builder() + .setTitle(getTitleFromProgram(mRecordedProgram, channel)) + .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis()) + .setDescription(description) + .setImageUris(mRecordedProgram, channel) + .build(); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, + res.getString(R.string.dvr_detail_resume_play), null, + res.getDrawable(R.drawable.lb_ic_play))); + adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_play_from_beginning), null, + res.getDrawable(R.drawable.lb_ic_replay))); + } else { + adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_watch), null, + res.getDrawable(R.drawable.lb_ic_play))); + } + adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING, + res.getString(R.string.dvr_detail_delete), null, + res.getDrawable(R.drawable.ic_delete_32dp))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); + } else if (action.getId() == ACTION_RESUME_PLAYING) { + startPlayback(mRecordedProgram, mDvrWatchedPositionManager + .getWatchedPosition(mRecordedProgram.getId())); + } else if (action.getId() == ACTION_DELETE_RECORDING) { + DvrManager dvrManager = TvApplication + .getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedProgram(mRecordedProgram); + getActivity().finish(); + } + } + }; + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (recordedProgram.getId() == mRecordedProgram.getId()) { + getActivity().finish(); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java new file mode 100644 index 00000000..ee978797 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java @@ -0,0 +1,179 @@ +/* + * 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.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.view.ViewGroup; + +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.DvrWatchedPositionManager; +import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.Utils; + +/** + * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. + */ +public class RecordedProgramPresenter extends DvrItemPresenter { + private final ChannelDataManager mChannelDataManager; + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private final Context mContext; + private String mTodayString; + private String mYesterdayString; + private final int mProgressBarColor; + private final boolean mShowEpisodeTitle; + private final boolean mExpandTitleWhenFocused; + + private static final class RecordedProgramViewHolder extends ViewHolder + implements WatchedPositionChangedListener { + private RecordedProgram mProgram; + + RecordedProgramViewHolder(RecordingCardView view, int progressColor) { + super(view); + view.setProgressBarColor(progressColor); + } + + private void setProgram(RecordedProgram program) { + mProgram = program; + } + + private void setProgressBar(long watchedPositionMs) { + ((RecordingCardView) view).setProgressBar( + (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null + : Math.min(100, (int) (100.0f * watchedPositionMs + / mProgram.getDurationMillis()))); + } + + @Override + public void onWatchedPositionChanged(long programId, long positionMs) { + if (programId == mProgram.getId()) { + setProgressBar(positionMs); + } + } + } + + public RecordedProgramPresenter(Context context, boolean showEpisodeTitle, + boolean expandTitleWhenFocused) { + mContext = context; + mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); + mTodayString = mContext.getString(R.string.dvr_date_today); + mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); + mDvrWatchedPositionManager = + 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, false); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused); + return new RecordedProgramViewHolder(view, mProgressBarColor); + } + + @Override + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + final RecordedProgram program = (RecordedProgram) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + Channel channel = mChannelDataManager.getChannel(program.getChannelId()); + String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) + : program.getTitleWithEpisodeNumber(mContext); + SpannableString title = titleString == null ? null : new SpannableString(titleString); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : mContext.getResources().getString(R.string.no_program_information)); + } else if (!mShowEpisodeTitle) { + // TODO: Some translation may add delimiters in-between program titles, we should use + // a more robust way to get the span range. + String programTitle = program.getTitle(); + title.setSpan(new TextAppearanceSpan(mContext, + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + cardView.setTitle(title); + String imageUri = null; + boolean isChannelLogo = false; + if (program.getPosterArtUri() != null) { + imageUri = program.getPosterArtUri(); + } else if (program.getThumbnailUri() != null) { + imageUri = program.getThumbnailUri(); + } else if (channel != null) { + imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); + isChannelLogo = true; + } + cardView.setImageUri(imageUri, isChannelLogo); + int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); + String durationString = getContext().getResources().getQuantityString( + R.plurals.dvr_program_duration, durationMinutes, durationMinutes); + cardView.setContent(getDescription(program), durationString); + if (viewHolder instanceof RecordedProgramViewHolder) { + RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; + cardViewHolder.setProgram(program); + mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId()); + cardViewHolder + .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); + } + super.onBindViewHolder(viewHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + if (viewHolder instanceof RecordedProgramViewHolder) { + mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder, + ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); + } + ((RecordingCardView) viewHolder.view).reset(); + super.onUnbindViewHolder(viewHolder); + } + + /** + * Returns description would be used in its card view. + */ + protected String getDescription(RecordedProgram recording) { + int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(), + System.currentTimeMillis()); + if (dateDifference == 0) { + return mTodayString; + } else if (dateDifference == 1) { + return mYesterdayString; + } else { + return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(), + recording.getStartTimeUtcMillis(), false, true, false, 0); + } + } + + /** + * Returns context. + */ + protected Context getContext() { + return mContext; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java new file mode 100644 index 00000000..7b0a8cb9 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -0,0 +1,264 @@ +/* + * 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.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.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.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.data.ScheduledRecording} + * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. + */ +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 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; + + public RecordingCardView(Context context) { + this(context, false); + } + + 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); + setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); + setFocusable(true); + setFocusableInTouchMode(true); + mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(R.layout.dvr_recording_card_view, this); + mImageView = (ImageView) findViewById(R.id.image); + mImageWidth = imageWidth; + mImageHeight = imageHeight; + mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); + mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); + mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); + 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) { + mFoldedTitleView.setText(title); + mExpandedTitleView.setText(title); + } + + void setContent(CharSequence majorContent, CharSequence minorContent) { + if (!TextUtils.isEmpty(majorContent)) { + mMajorContentView.setText(majorContent); + mMajorContentView.setVisibility(View.VISIBLE); + } else { + mMajorContentView.setVisibility(View.GONE); + } + if (!TextUtils.isEmpty(minorContent)) { + mMinorContentView.setText(minorContent); + mMinorContentView.setVisibility(View.VISIBLE); + } else { + mMinorContentView.setVisibility(View.GONE); + } + } + + /** + * Sets progress bar. If progress is {@code null}, hides progress bar. + */ + void setProgressBar(Integer progress) { + if (progress == null) { + mProgressBar.setVisibility(View.GONE); + } else { + mProgressBar.setProgress(progress); + mProgressBar.setVisibility(View.VISIBLE); + } + } + + /** + * Sets the color of progress bar. + */ + void setProgressBarColor(int color) { + mProgressBar.getProgressDrawable().setTint(color); + } + + void setImageUri(String uri, boolean isChannelLogo) { + if (isChannelLogo) { + mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + } else { + mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + } + mImageUri = uri; + if (TextUtils.isEmpty(uri)) { + mImageView.setImageDrawable(mDefaultImage); + } else { + ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight, + new RecordingCardImageLoaderCallback(this, uri)); + } + } + + /** + * Set image to card view. + */ + public void setImage(Drawable image) { + if (image != null) { + mImageView.setImageDrawable(image); + } + } + + public void setAffiliatedIcon(int imageResId) { + if (imageResId > 0) { + mAffiliatedIconContainer.setVisibility(View.VISIBLE); + mAffiliatedIcon.setImageResource(imageResId); + } else { + mAffiliatedIconContainer.setVisibility(View.INVISIBLE); + } + } + + /** + * Returns image view. + */ + public ImageView getImageView() { + return mImageView; + } + + private static class RecordingCardImageLoaderCallback + extends ImageLoader.ImageLoaderCallback { + private final String mUri; + + RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) { + super(referent); + mUri = uri; + } + + @Override + public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) { + if (bitmap == null || !mUri.equals(view.mImageUri)) { + view.mImageView.setImageDrawable(view.mDefaultImage); + } else { + view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap)); + } + } + } + + public void reset() { + 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/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java new file mode 100644 index 00000000..a877e05f --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -0,0 +1,87 @@ +/* + * 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.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.dvr.data.ScheduledRecording; + +/** + * {@link DetailsFragment} for recordings in DVR. + */ +abstract class RecordingDetailsFragment extends DvrDetailsFragment { + private ScheduledRecording mRecording; + + @Override + protected void onCreateInternal() { + setDetailsOverviewRow(createDetailsContent()); + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() + .getScheduledRecording(scheduledRecordingId); + return mRecording != null; + } + + /** + * Returns {@link ScheduledRecording} for the current fragment. + */ + public ScheduledRecording getRecording() { + return mRecording; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mRecording.getChannelId()); + SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ? + null : new SpannableString(mRecording + .getProgramTitleWithEpisodeNumber(getContext())); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : getContext().getResources().getString( + R.string.no_program_information)); + } else { + String programTitle = mRecording.getProgramTitle(); + title.setSpan(new TextAppearanceSpan(getContext(), + R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 + : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ? + mRecording.getProgramDescription() : mRecording.getProgramLongDescription(); + if (TextUtils.isEmpty(description)) { + description = channel != null ? channel.getDescription() : null; + } + return new DetailsContent.Builder() + .setTitle(title) + .setStartTimeUtcMillis(mRecording.getStartTimeMs()) + .setEndTimeUtcMillis(mRecording.getEndTimeMs()) + .setDescription(description) + .setImageUris(mRecording.getProgramPosterArtUri(), + mRecording.getProgramThumbnailUri(), channel) + .build(); + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java new file mode 100644 index 00000000..eb0f4f0d --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.browse; + +import android.content.res.Resources; +import android.os.Bundle; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** + * {@link RecordingDetailsFragment} for scheduled recording in DVR. + */ +public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_VIEW_SCHEDULE = 1; + private static final int ACTION_CANCEL = 2; + + private DvrManager mDvrManager; + private Action mScheduleAction; + private boolean mHideViewSchedule; + + @Override + public void onCreate(Bundle savedInstance) { + mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); + super.onCreate(savedInstance); + } + + @Override + public void onResume() { + super.onResume(); + if (mScheduleAction != null) { + mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId())); + } + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + if (!mHideViewSchedule) { + mScheduleAction = new Action(ACTION_VIEW_SCHEDULE, + res.getString(R.string.dvr_detail_view_schedule), null, + res.getDrawable(getScheduleIconId())); + adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); + } + adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, + res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null, + res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + long actionId = action.getId(); + if (actionId == ACTION_VIEW_SCHEDULE) { + DvrUiHelper.startSchedulesActivity(getContext(), getRecording()); + } else if (actionId == ACTION_CANCEL) { + mDvrManager.removeScheduledRecording(getRecording()); + getActivity().finish(); + } + } + }; + } + + private int getScheduleIconId() { + if (mDvrManager.isConflicting(getRecording())) { + return R.drawable.ic_warning_white_32dp; + } else { + return R.drawable.ic_schedule_32dp; + } + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java new file mode 100644 index 00000000..efc8785a --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -0,0 +1,174 @@ +/* + * 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.media.tv.TvContract; +import android.os.Handler; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; +import android.view.ViewGroup; + +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.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.util.Utils; + +import java.util.concurrent.TimeUnit; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +class ScheduledRecordingPresenter extends DvrItemPresenter { + private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); + + private final ChannelDataManager mChannelDataManager; + private final DvrManager mDvrManager; + private final Context mContext; + private final int mProgressBarColor; + + private static final class ScheduledRecordingViewHolder extends ViewHolder { + private final Handler mHandler = new Handler(); + private ScheduledRecording mScheduledRecording; + private final Runnable mProgressBarUpdater = new Runnable() { + @Override + public void run() { + updateProgressBar(); + mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); + } + }; + + ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { + super(view); + view.setProgressBarColor(progressBarColor); + } + + private void updateProgressBar() { + if (mScheduledRecording == null) { + return; + } + int recordingState = mScheduledRecording.getState(); + RecordingCardView cardView = (RecordingCardView) view; + if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + cardView.setProgressBar(Math.max(0, Math.min((int) (100 * + (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) + / mScheduledRecording.getDuration()), 100))); + } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { + cardView.setProgressBar(100); + } else { + // Hides progress bar. + cardView.setProgressBar(null); + } + } + + private void startUpdateProgressBar() { + mHandler.post(mProgressBarUpdater); + } + + private void stopUpdateProgressBar() { + mHandler.removeCallbacks(mProgressBarUpdater); + } + } + + public ScheduledRecordingPresenter(Context context) { + mContext = context; + ApplicationSingletons singletons = TvApplication.getSingletons(mContext); + mChannelDataManager = singletons.getChannelDataManager(); + mDvrManager = singletons.getDvrManager(); + mProgressBarColor = mContext.getResources() + .getColor(R.color.play_controls_recording_icon_color_on_focus); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView(mContext); + return new ScheduledRecordingViewHolder(view, mProgressBarColor); + } + + @Override + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final ScheduledRecording recording = (ScheduledRecording) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + final Context context = viewHolder.view.getContext(); + + setTitleAndImage(cardView, recording); + int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), + recording.getStartTimeMs()); + if (dateDifference <= 0) { + cardView.setContent(mContext.getString(R.string.dvr_date_today_time, + Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)), null); + } else if (dateDifference == 1) { + cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time, + Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)), null); + } else { + cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), + recording.getStartTimeMs(), false, true, false, 0), null); + } + if (mDvrManager.isConflicting(recording)) { + cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); + } else { + cardView.setAffiliatedIcon(0); + } + viewHolder.updateProgressBar(); + viewHolder.mScheduledRecording = recording; + viewHolder.startUpdateProgressBar(); + super.onBindViewHolder(viewHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder baseHolder) { + ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + viewHolder.stopUpdateProgressBar(); + viewHolder.mScheduledRecording = null; + ((RecordingCardView) viewHolder.view).reset(); + super.onUnbindViewHolder(viewHolder); + } + + private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ? + null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext)); + if (TextUtils.isEmpty(title)) { + title = new SpannableString(channel != null ? channel.getDisplayName() + : mContext.getResources().getString(R.string.no_program_information)); + } else { + String programTitle = recording.getProgramTitle(); + title.setSpan(new TextAppearanceSpan(mContext, + R.style.text_appearance_card_view_episode_number), + programTitle == null ? 0 : programTitle.length(), title.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + String imageUri = recording.getProgramPosterArtUri(); + boolean isChannelLogo = false; + if (TextUtils.isEmpty(imageUri)) { + imageUri = channel != null ? + TvContract.buildChannelLogoUri(channel.getId()).toString() : null; + isChannelLogo = true; + } + cardView.setTitle(title); + cardView.setImageUri(imageUri, isChannelLogo); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java new file mode 100644 index 00000000..f7b60b50 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -0,0 +1,369 @@ +/* + * 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.res.Resources; +import android.graphics.drawable.Drawable; +import android.media.tv.TvInputManager; +import android.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; +import android.support.v17.leanback.widget.Action; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.DetailsOverviewRow; +import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.PresenterSelector; +import android.support.v17.leanback.widget.SparseArrayObjectAdapter; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +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; +import java.util.List; + +/** + * {@link DetailsFragment} for series recording in DVR. + */ +public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements + DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { + private static final int ACTION_WATCH = 1; + private static final int ACTION_SERIES_SCHEDULES = 2; + private static final int ACTION_DELETE = 3; + + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private DvrDataManager mDvrDataManager; + + private SeriesRecording mSeries; + // NOTICE: mRecordedPrograms should only be used in creating details fragments. + // After fragments are created, it should be cleared to save resources. + private List mRecordedPrograms; + private RecordedProgram mRecommendRecordedProgram; + private DetailsContent mDetailsContent; + private int mSeasonRowCount; + private SparseArrayObjectAdapter mActionsAdapter; + private Action mDeleteAction; + + private boolean mPaused; + private long mInitialPlaybackPositionMs; + private String mWatchLabel; + private String mResumeLabel; + private Drawable mWatchDrawable; + private RecordedProgramPresenter mRecordedProgramPresenter; + + @Override + public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mWatchLabel = getString(R.string.dvr_detail_watch); + mResumeLabel = getString(R.string.dvr_detail_series_resume); + mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); + super.onCreate(savedInstanceState); + } + + @Override + protected void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); + setDetailsOverviewRow(mDetailsContent); + setupRecordedProgramsRow(); + mDvrDataManager.addSeriesRecordingListener(this); + mDvrDataManager.addRecordedProgramListener(this); + mRecordedPrograms = null; + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateWatchAction(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + private void updateWatchAction() { + List programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); + mRecommendRecordedProgram = getRecommendProgram(programs); + if (mRecommendRecordedProgram == null) { + mActionsAdapter.clear(ACTION_WATCH); + } else { + String episodeStatus; + if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + episodeStatus = mResumeLabel; + mInitialPlaybackPositionMs = mDvrWatchedPositionManager + .getWatchedPosition(mRecommendRecordedProgram.getId()); + } else { + episodeStatus = mWatchLabel; + mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( + getContext()); + mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, + episodeStatus, episodeDisplayNumber, mWatchDrawable)); + } + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() + .getSeriesRecording(recordId); + if (mSeries == null) { + return false; + } + mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); + mDetailsContent = createDetailsContent(); + return true; + } + + @Override + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { + ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); + presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); + presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + return presenterSelector; + } + + private DetailsContent createDetailsContent() { + Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() + .getChannel(mSeries.getChannelId()); + String description = TextUtils.isEmpty(mSeries.getLongDescription()) + ? mSeries.getDescription() : mSeries.getLongDescription(); + return new DetailsContent.Builder() + .setTitle(mSeries.getTitle()) + .setDescription(description) + .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel) + .build(); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + updateWatchAction(); + mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, + getString(R.string.dvr_detail_view_schedule), null, + res.getDrawable(R.drawable.ic_schedule_32dp, null))); + mDeleteAction = new Action(ACTION_DELETE, + getString(R.string.dvr_detail_series_delete), null, + res.getDrawable(R.drawable.ic_delete_32dp, null)); + if (!mRecordedPrograms.isEmpty()) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } + return mActionsAdapter; + } + + private void setupRecordedProgramsRow() { + for (RecordedProgram program : mRecordedPrograms) { + addProgram(program); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mDvrDataManager.removeSeriesRecordingListener(this); + mDvrDataManager.removeRecordedProgramListener(this); + if (mSeries != null) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); + } + mRecordedProgramPresenter.unbindAllViewHolders(); + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_WATCH) { + startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); + } else if (action.getId() == ACTION_SERIES_SCHEDULES) { + DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); + } else if (action.getId() == ACTION_DELETE) { + DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); + } + } + }; + } + + /** + * The programs are sorted by season number and episode number. + */ + private RecordedProgram getRecommendProgram(List programs) { + for (int i = programs.size() - 1 ; i >= 0 ; i--) { + RecordedProgram program = programs.get(i); + int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { + continue; + } + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + return program; + } + if (i == programs.size() - 1) { + return program; + } else { + return programs.get(i + 1); + } + } + return programs.isEmpty() ? null : programs.get(0); + } + + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (mSeries.getId() == series.getId()) { + mSeries = series; + } + } + } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeries.getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + addProgram(recordedProgram); + if (mActionsAdapter.lookup(ACTION_DELETE) == null) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } + } + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + // Do nothing + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); + if (row != null) { + SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); + adapter.remove(recordedProgram); + if (adapter.isEmpty()) { + getRowsAdapter().remove(row); + if (getRowsAdapter().size() == 1) { + // No season rows left. Only DetailsOverviewRow + mActionsAdapter.clear(ACTION_DELETE); + } + } + } + if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + updateWatchAction(); + } + } + } + } + + private void addProgram(RecordedProgram program) { + String programSeasonNumber = + TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber(); + getOrCreateSeasonRowAdapter(programSeasonNumber).add(program); + } + + private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) { + ListRow row = getSeasonRow(seasonNumber, true); + return (SeasonRowAdapter) row.getAdapter(); + } + + private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) { + seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber; + ArrayObjectAdapter rowsAdaptor = getRowsAdapter(); + for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { + Object row = rowsAdaptor.get(i); + if (row instanceof ListRow) { + int compareResult = BaseProgram.numberCompare(seasonNumber, + ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); + if (compareResult == 0) { + return (ListRow) row; + } else if (compareResult < 0) { + return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null; + } + } + } + return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null; + } + + private ListRow createNewSeasonRow(String seasonNumber, int position) { + String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle() + : getString(R.string.dvr_detail_series_season_title, seasonNumber); + HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); + ClassPresenterSelector selector = new ClassPresenterSelector(); + selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); + ListRow row = new ListRow(header, new SeasonRowAdapter(selector, + new Comparator() { + @Override + public int compare(RecordedProgram lhs, RecordedProgram rhs) { + return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); + } + }, seasonNumber)); + getRowsAdapter().add(position, row); + return row; + } + + private class SeasonRowAdapter extends SortedArrayAdapter { + private String mSeasonNumber; + + SeasonRowAdapter(PresenterSelector selector, Comparator comparator, + String seasonNumber) { + super(selector, comparator); + mSeasonNumber = seasonNumber; + } + + @Override + public long getId(RecordedProgram program) { + return program.getId(); + } + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java new file mode 100644 index 00000000..af6ecc19 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java @@ -0,0 +1,233 @@ +/* + * 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.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.text.TextUtils; +import android.view.ViewGroup; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.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}. + */ +class SeriesRecordingPresenter extends DvrItemPresenter { + private final ChannelDataManager mChannelDataManager; + private final DvrDataManager mDvrDataManager; + private final DvrManager mDvrManager; + private final DvrWatchedPositionManager mWatchedPositionManager; + + private static final class SeriesRecordingViewHolder extends ViewHolder implements + WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { + private SeriesRecording mSeriesRecording; + private RecordingCardView mCardView; + private DvrDataManager mDvrDataManager; + private DvrManager mDvrManager; + private DvrWatchedPositionManager mWatchedPositionManager; + + SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, + DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { + super(view); + mCardView = view; + mDvrDataManager = dvrDataManager; + mDvrManager = dvrManager; + mWatchedPositionManager = watchedPositionManager; + } + + @Override + public void onWatchedPositionChanged(long recordedProgramId, long positionMs) { + if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgramId); + updateCardViewContent(); + } + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduledRecording : scheduledRecordings) { + if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { + updateCardViewContent(); + return; + } + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + for (ScheduledRecording scheduledRecording : scheduledRecordings) { + if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) { + updateCardViewContent(); + return; + } + } + } + + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + mDvrDataManager.removeScheduledRecordingListener(this); + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { + updateCardViewContent(); + } + } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + } + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { + updateCardViewContent(); + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + // Do nothing + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { + // Do nothing + } + + public void onBound(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addRecordedProgramListener(this); + for (RecordedProgram recordedProgram : + mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + } + } + updateCardViewContent(); + } + + public void onUnbound() { + mDvrDataManager.removeScheduledRecordingListener(this); + mDvrDataManager.removeRecordedProgramListener(this); + mWatchedPositionManager.removeListener(this); + } + + private void updateCardViewContent() { + int count = 0; + int quantityStringID; + List recordedPrograms = + mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); + if (recordedPrograms.size() == 0) { + count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + quantityStringID = R.plurals.dvr_count_scheduled_recordings; + } else { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + count++; + } + } + if (count == 0) { + count = recordedPrograms.size(); + quantityStringID = R.plurals.dvr_count_recordings; + } else { + quantityStringID = R.plurals.dvr_count_new_recordings; + } + } + mCardView.setContent(mCardView.getResources() + .getQuantityString(quantityStringID, count, count), null); + } + } + + public SeriesRecordingPresenter(Context context) { + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mChannelDataManager = singletons.getChannelDataManager(); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrManager = singletons.getDvrManager(); + mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + Context context = parent.getContext(); + RecordingCardView view = new RecordingCardView(context); + return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, + mWatchedPositionManager); + } + + @Override + public void onBindViewHolder(ViewHolder baseHolder, Object o) { + final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder; + final SeriesRecording seriesRecording = (SeriesRecording) o; + final RecordingCardView cardView = (RecordingCardView) viewHolder.view; + viewHolder.onBound(seriesRecording); + setTitleAndImage(cardView, seriesRecording); + super.onBindViewHolder(baseHolder, o); + } + + @Override + public void onUnbindViewHolder(ViewHolder viewHolder) { + ((RecordingCardView) viewHolder.view).reset(); + ((SeriesRecordingViewHolder) viewHolder).onUnbound(); + super.onUnbindViewHolder(viewHolder); + } + + private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { + cardView.setTitle(recording.getTitle()); + if (recording.getPosterUri() != null) { + cardView.setImageUri(recording.getPosterUri(), false); + } else { + Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); + String imageUri = null; + if (channel != null) { + imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); + } + cardView.setImageUri(imageUri, true); + } + } +} diff --git a/src/com/android/tv/dvr/ui/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/list/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java new file mode 100644 index 00000000..a0410bb3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.os.Bundle; +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.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.Collections; +import java.util.List; + +/** + * Activity to show the list of recording schedules. + */ +public class DvrSchedulesActivity extends Activity { + /** + * The key for the type of the schedules which will be listed in the list. The type of the value + * should be {@link ScheduleListType}. + */ + public static final String KEY_SCHEDULES_TYPE = "schedules_type"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_FULL_SCHEDULE, TYPE_SERIES_SCHEDULE}) + public @interface ScheduleListType {} + /** + * A type which means the activity will display the full scheduled recordings. + */ + public static final int TYPE_FULL_SCHEDULE = 0; + /** + * A type which means the activity will display a scheduled recording list of a series + * recording. + */ + public static final int TYPE_SERIES_SCHEDULE = 1; + + @Override + public void onCreate(final Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + // Pass null to prevent automatically re-creating fragments + super.onCreate(null); + setContentView(R.layout.activity_dvr_schedules); + int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); + if (scheduleType == TYPE_FULL_SCHEDULE) { + DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); + schedulesFragment.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } else if (scheduleType == TYPE_SERIES_SCHEDULE) { + 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 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 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) 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 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 mPrograms; public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series) { + SeriesRecording series, List programs) { super(title, description, itemCount); mSeriesRecording = series; + mPrograms = programs; + } + + /** + * Returns the list of programs which belong to the series. + */ + public List 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 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/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java new file mode 100644 index 00000000..2437d1f5 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -0,0 +1,67 @@ +/* + * 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.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.data.RecordedProgram; + +/** + * Activity to play a {@link RecordedProgram}. + */ +public class DvrPlaybackActivity extends Activity { + private static final String TAG = "DvrPlaybackActivity"; + private static final boolean DEBUG = false; + + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_dvr_playback); + mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment); + } + + @Override + public void onVisibleBehindCanceled() { + if (DEBUG) Log.d(TAG, "onVisibleBehindCanceled"); + super.onVisibleBehindCanceled(); + finish(); + } + + @Override + protected void onNewIntent(Intent intent) { + mOverlayFragment.onNewIntent(intent); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + float density = getResources().getDisplayMetrics().density; + mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), + (int) (newConfig.screenHeightDp * density)); + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java new file mode 100644 index 00000000..4bd121b1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -0,0 +1,77 @@ +/* + * 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.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import com.android.tv.R; +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. + */ +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, DvrPlaybackOverlayFragment fragment) { + super(context); + mFragment = fragment; + mRelatedRecordingCardWidth = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); + mRelatedRecordingCardHeight = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent) { + RecordingCardView view = new RecordingCardView( + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true); + return new ViewHolder(view); + } + + @Override + protected OnClickListener onCreateOnClickListener() { + 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); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + }; + } +} \ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java new file mode 100644 index 00000000..4658a328 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -0,0 +1,399 @@ +/* + * 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.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; +import android.util.Log; +import android.view.KeyEvent; +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. + */ +class DvrPlaybackControlHelper extends PlaybackControlGlue { + private static final String TAG = "DvrPlaybackControlHelper"; + private static final boolean DEBUG = false; + + private static final int AUDIO_ACTION_ID = 1001; + + private int mPlaybackState = PlaybackState.STATE_NONE; + private int mPlaybackSpeedLevel; + private int mPlaybackSpeedId; + private boolean mReadyToControl; + + private final MediaController mMediaController; + private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); + private final TransportControls mTransportControls; + private final int mExtraPaddingTopForNoDescription; + 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]); + mMediaController = activity.getMediaController(); + mMediaController.registerCallback(mMediaControllerCallback); + 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() { + @Override + protected void onBindDescription( + AbstractDetailsDescriptionPresenter.ViewHolder viewHolder, Object object) { + PlaybackControlGlue glue = (PlaybackControlGlue) object; + if (glue.hasValidMedia()) { + viewHolder.getTitle().setText(glue.getMediaTitle()); + viewHolder.getSubtitle().setText(glue.getMediaSubtitle()); + } else { + viewHolder.getTitle().setText(""); + viewHolder.getSubtitle().setText(""); + } + if (TextUtils.isEmpty(viewHolder.getSubtitle().getText())) { + viewHolder.view.setPadding(viewHolder.view.getPaddingLeft(), + mExtraPaddingTopForNoDescription, + viewHolder.view.getPaddingRight(), viewHolder.view.getPaddingBottom()); + } + } + }; + PlaybackControlsRowPresenter presenter = + new PlaybackControlsRowPresenter(detailsPresenter) { + @Override + protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + vh.setOnKeyListener(DvrPlaybackControlHelper.this); + } + + @Override + protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) { + super.onUnbindRowViewHolder(vh); + vh.setOnKeyListener(null); + } + }; + presenter.setProgressColor(getContext().getResources() + .getColor(R.color.play_controls_progress_bar_watched)); + presenter.setBackgroundColor(getContext().getResources() + .getColor(R.color.play_controls_body_background_enabled)); + presenter.setOnActionClickedListener(new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (mReadyToControl) { + 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 trackInfos = + ((DvrPlaybackOverlayFragment) getFragment()).getTracks(trackType); + if (!trackInfos.isEmpty()) { + showSideFragment(trackInfos, ((DvrPlaybackOverlayFragment) + getFragment()).getSelectedTrackId(trackType)); + } + } + } + }); + return presenter; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (mReadyToControl) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN + && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING)) { + // Workaround of b/31489271. Clicks play/pause button first to reset play controls + // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. + onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + } + return super.onKey(v, keyCode, event); + } + return false; + } + + @Override + public boolean hasValidMedia() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + return playbackState != null; + } + + @Override + public boolean isMediaPlaying() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return false; + } + int state = playbackState.getState(); + return state != PlaybackState.STATE_NONE && state != PlaybackState.STATE_CONNECTING + && state != PlaybackState.STATE_PAUSED; + } + + /** + * Returns the ID of the media under playback. + */ + public String getMediaId() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? null + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); + } + + @Override + public CharSequence getMediaTitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + } + + @Override + public CharSequence getMediaSubtitle() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? "" + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); + } + + @Override + public int getMediaDuration() { + MediaMetadata mediaMetadata = mMediaController.getMetadata(); + return mediaMetadata == null ? 0 + : (int) mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION); + } + + @Override + public Drawable getMediaArt() { + // Do not show the poster art on control row. + return null; + } + + @Override + public long getSupportedActions() { + return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND; + } + + @Override + public int getCurrentSpeedId() { + return mPlaybackSpeedId; + } + + @Override + public int getCurrentPosition() { + PlaybackState playbackState = mMediaController.getPlaybackState(); + if (playbackState == null) { + return 0; + } + return (int) playbackState.getPosition(); + } + + /** + * Unregister media controller's callback. + */ + public void unregisterCallback() { + mMediaController.unregisterCallback(mMediaControllerCallback); + } + + /** + * 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) { + return; + } + if (speedId == PLAYBACK_SPEED_NORMAL) { + mTransportControls.play(); + } else if (speedId <= -PLAYBACK_SPEED_FAST_L0) { + mTransportControls.rewind(); + } else if (speedId >= PLAYBACK_SPEED_FAST_L0){ + mTransportControls.fastForward(); + } + } + + @Override + protected void pausePlayback() { + mTransportControls.pause(); + } + + @Override + protected void skipToNext() { + // Do nothing. + } + + @Override + protected void skipToPrevious() { + // Do nothing. + } + + @Override + protected void onRowChanged(PlaybackControlsRow row) { + // Do nothing. + } + + /** + * 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); + if (state == mPlaybackState && mPlaybackSpeedLevel == speedLevel) { + // Only position is changed, no need to update controls row + return; + } + // NOTICE: The below two variables should only be used in this method. + // The only usage of them is to confirm if the state is changed or not. + mPlaybackState = state; + mPlaybackSpeedLevel = speedLevel; + switch (state) { + case PlaybackState.STATE_PLAYING: + mPlaybackSpeedId = PLAYBACK_SPEED_NORMAL; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_PAUSED: + mPlaybackSpeedId = PLAYBACK_SPEED_PAUSED; + setFadingEnabled(true); + mReadyToControl = true; + break; + case PlaybackState.STATE_FAST_FORWARDING: + mPlaybackSpeedId = PLAYBACK_SPEED_FAST_L0 + speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_REWINDING: + mPlaybackSpeedId = -PLAYBACK_SPEED_FAST_L0 - speedLevel; + setFadingEnabled(false); + mReadyToControl = true; + break; + case PlaybackState.STATE_CONNECTING: + setFadingEnabled(false); + mReadyToControl = false; + break; + case PlaybackState.STATE_NONE: + mReadyToControl = false; + break; + default: + setFadingEnabled(true); + break; + } + onStateChanged(); + } + + private void showSideFragment(ArrayList 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) { + if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); + onStateChanged(state.getState(), state.getPosition(), (int) state.getPlaybackSpeed()); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + DvrPlaybackControlHelper.this.onMetadataChanged(); + ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); + } + } + + 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/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java new file mode 100644 index 00000000..843d2dbe --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -0,0 +1,335 @@ +/* + * 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.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadata; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.media.tv.TvContract; +import android.os.AsyncTask; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +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; + +class DvrPlaybackMediaSessionHelper { + private static final String TAG = "DvrPlaybackMediaSessionHelper"; + private static final boolean DEBUG = false; + + private int mNowPlayingCardWidth; + private int mNowPlayingCardHeight; + private int mSpeedLevel; + private long mProgramDurationMs; + + private Activity mActivity; + private DvrPlayer mDvrPlayer; + private MediaSession mMediaSession; + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private final ChannelDataManager mChannelDataManager; + + public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, + DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { + mActivity = activity; + mDvrPlayer = dvrPlayer; + mDvrWatchedPositionManager = + TvApplication.getSingletons(activity).getDvrWatchedPositionManager(); + mChannelDataManager = TvApplication.getSingletons(activity).getChannelDataManager(); + mDvrPlayer.setCallback(new DvrPlayer.DvrPlayerCallback() { + @Override + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { + updateMediaSessionPlaybackState(); + } + + @Override + public void onPlaybackPositionChanged(long positionMs) { + updateMediaSessionPlaybackState(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrWatchedPositionManager + .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); + } + } + + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } + }); + initializeMediaSession(mediaSessionTag); + } + + /** + * Stops DVR player and release media session. + */ + public void release() { + if (mDvrPlayer != null) { + mDvrPlayer.reset(); + } + if (mMediaSession != null) { + mMediaSession.release(); + mMediaSession = null; + } + } + + /** + * Updates media session's playback state and speed. + */ + public void updateMediaSessionPlaybackState() { + mMediaSession.setPlaybackState(new PlaybackState.Builder() + .setState(mDvrPlayer.getPlaybackState(), mDvrPlayer.getPlaybackPosition(), + mSpeedLevel).build()); + } + + /** + * Sets the recorded program for playback. + * + * @param program The recorded program to play. {@code null} to reset the DVR player. + */ + public void setupPlayback(RecordedProgram program, long seekPositionMs) { + if (program != null) { + mDvrPlayer.setProgram(program, seekPositionMs); + setupMediaSession(program); + } else { + mDvrPlayer.reset(); + mMediaSession.setActive(false); + } + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mDvrPlayer.getProgram(); + } + + /** + * Checks if the recorded program is the same as now playing one. + */ + public boolean isCurrentProgram(RecordedProgram program) { + return program != null && program.equals(getProgram()); + } + + /** + * Returns playback state. + */ + public int getPlaybackState() { + return mDvrPlayer.getPlaybackState(); + } + + /** + * Returns the underlying DVR player. + */ + public DvrPlayer getDvrPlayer() { + return mDvrPlayer; + } + + private void initializeMediaSession(String mediaSessionTag) { + mMediaSession = new MediaSession(mActivity, mediaSessionTag); + mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mNowPlayingCardWidth = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_max_width); + mNowPlayingCardHeight = mActivity.getResources() + .getDimensionPixelSize(R.dimen.notif_card_img_height); + mMediaSession.setCallback(new MediaSessionCallback()); + mActivity.setMediaController( + new MediaController(mActivity, mMediaSession.getSessionToken())); + updateMediaSessionPlaybackState(); + } + + private void setupMediaSession(RecordedProgram program) { + mProgramDurationMs = program.getDurationMillis(); + String cardTitleText = program.getTitle(); + if (TextUtils.isEmpty(cardTitleText)) { + Channel channel = mChannelDataManager.getChannel(program.getChannelId()); + cardTitleText = (channel != null) ? channel.getDisplayName() + : mActivity.getString(R.string.no_program_information); + } + 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, currentMetadata, null, posterArtUri); + mMediaSession.setActive(true); + } + + private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, + @Nullable Bitmap posterArt, @Nullable String posterArtUri) { + if (posterArt != null) { + updateMetadataImageInfo(program, currentMetadata, posterArt, 0); + } else if (posterArtUri != null) { + ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, + mNowPlayingCardHeight, + new ProgramPosterArtCallback(mActivity, program, currentMetadata)); + } else { + updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); + } + } + + private class ProgramPosterArtCallback extends + ImageLoader.ImageLoaderCallback { + private final RecordedProgram mRecordedProgram; + private final MediaMetadata mCurrentMetadata; + + public ProgramPosterArtCallback(Activity activity, RecordedProgram program, + MediaMetadata metadata) { + super(activity); + mRecordedProgram = program; + mCurrentMetadata = metadata; + } + + @Override + public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { + if (isCurrentProgram(mRecordedProgram)) { + updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); + } + } + } + + 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()); + } else { + new AsyncTask() { + @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(); + } + } + } + + // An event was triggered by MediaController.TransportControls and must be handled here. + // Here we update the media itself to act on the event that was triggered. + private class MediaSessionCallback extends MediaSession.Callback { + @Override + public void onPrepare() { + if (!mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.prepare(true); + } + } + + @Override + public void onPlay() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.play(); + } + } + + @Override + public void onPause() { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.pause(); + } + } + + @Override + public void onFastForward() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.fastForward( + TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onRewind() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } + if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { + if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { + mSpeedLevel++; + } else { + return; + } + } else { + mSpeedLevel = 0; + } + mDvrPlayer.rewind(TimeShiftUtils.getPlaybackSpeed(mSpeedLevel, mProgramDurationMs)); + } + + @Override + public void onSeekTo(long positionMs) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.seekTo(positionMs); + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java new file mode 100644 index 00000000..ff907182 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -0,0 +1,431 @@ +/* + * 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.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; +import android.media.tv.TvView; +import android.support.v17.leanback.app.PlaybackOverlayFragment; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.SinglePresenterSelector; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; +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"; + private static final boolean DEBUG = false; + + private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; + private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + + // mProgram is only used to store program from intent. Don't use it elsewhere. + private RecordedProgram mProgram; + private DvrPlayer mDvrPlayer; + private DvrPlaybackMediaSessionHelper mMediaSessionHelper; + private DvrPlaybackControlHelper mPlaybackControlHelper; + private ArrayObjectAdapter mRowsAdapter; + private SortedArrayAdapter mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; + private DvrDataManager mDvrDataManager; + private ContentRatingsManager mContentRatingsManager; + private TvView mTvView; + private View mBlockScreenView; + private ListRow mRelatedRecordingsRow; + 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); + 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(); + mProgram = getProgramFromIntent(getActivity().getIntent()); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + getActivity().finish(); + return; + } + Point size = new Point(); + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); + mWindowWidth = size.x; + mWindowHeight = size.y; + mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; + setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); + setFadingEnabled(true); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mBlockScreenView = getActivity().findViewById(R.id.block_screen); + mDvrPlayer = new DvrPlayer(mTvView); + mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); + setUpRows(); + 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); + } + }); + mPinChecked = getActivity().getIntent() + .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); + mDvrPlayer.setOnContentBlockedListener(new DvrPlayer.OnContentBlockedListener() { + @Override + public void onContentBlocked(TvContentRating rating) { + if (mPinChecked) { + mTvView.unblockContent(rating); + return; + } + mBlockScreenView.setVisibility(View.VISIBLE); + getActivity().getMediaController().getTransportControls().pause(); + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + mPinChecked = true; + mTvView.unblockContent(rating); + mBlockScreenView.setVisibility(View.GONE); + getActivity().getMediaController() + .getTransportControls().play(); + } + } + }, mContentRatingsManager.getDisplayNameForRating(rating)) + .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + }); + preparePlayback(getActivity().getIntent()); + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause"); + super.onPause(); + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING + || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { + getActivity().getMediaController().getTransportControls().pause(); + } + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { + getActivity().requestVisibleBehind(false); + } else { + getActivity().requestVisibleBehind(true); + } + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mPlaybackControlHelper.unregisterCallback(); + mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); + } + + /** + * Passes the intent to the fragment. + */ + public void onNewIntent(Intent intent) { + mProgram = getProgramFromIntent(intent); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + // Continue playing the original program + return; + } + preparePlayback(intent); + } + + /** + * Should be called when windows' size is changed in order to notify DVR player + * to update it's view width/height and position. + */ + public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; + updateAspectRatio(mAppliedAspectRatio); + } + + /** + * 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()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + + /** + * 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 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() { + 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 (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 { + int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); + } + mAppliedAspectRatio = videoAspectRatio; + } + + private void preparePlayback(Intent intent) { + mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); + mPlaybackControlHelper.updateSecondaryRow(false, false); + getActivity().getMediaController().getTransportControls().prepare(); + updateRelatedRecordingsRow(); + } + + private void updateRelatedRecordingsRow() { + boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); + mRelatedRecordingsRowAdapter.clear(); + long programId = mProgram.getId(); + String seriesId = mProgram.getSeriesId(); + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + if (seriesRecording != null) { + if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); + List relatedPrograms = + mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); + for (RecordedProgram program : relatedPrograms) { + if (programId != program.getId()) { + mRelatedRecordingsRowAdapter.add(program); + } + } + } + if (mRelatedRecordingsRowAdapter.size() == 0) { + mRowsAdapter.remove(mRelatedRecordingsRow); + } else if (wasEmpty){ + mRowsAdapter.add(mRelatedRecordingsRow); + } + 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() { + PlaybackControlsRowPresenter controlsRowPresenter = + mPlaybackControlHelper.createControlsRowAndPresenter(); + + ClassPresenterSelector selector = new ClassPresenterSelector(); + selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); + selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + + mRowsAdapter = new ArrayObjectAdapter(selector); + mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); + mRelatedRecordingsRow = getRelatedRecordingsRow(); + setAdapter(mRowsAdapter); + } + + private ListRow getRelatedRecordingsRow() { + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity(), this); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); + HeaderItem header = new HeaderItem(0, + getActivity().getString(R.string.dvr_playback_related_recordings)); + return new ListRow(header, mRelatedRecordingsRowAdapter); + } + + private RecordedProgram getProgramFromIntent(Intent intent) { + long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); + return mDvrDataManager.getRecordedProgram(programId); + } + + private long getSeekTimeFromIntent(Intent intent) { + return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, + TvInputManager.TIME_SHIFT_INVALID_TIME); + } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + public long getId(BaseProgram item) { + return item.getId(); + } + } +} \ No newline at end of file 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 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 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/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java new file mode 100644 index 00000000..780bfb2f --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -0,0 +1,583 @@ +/* + * 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.PlaybackParams; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.media.session.PlaybackState; +import android.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; + +class DvrPlayer { + private static final String TAG = "DvrPlayer"; + private static final boolean DEBUG = false; + + /** + * The max rewinding speed supported by DVR player. + */ + public static final int MAX_REWIND_SPEED = 256; + /** + * The max fast-forwarding speed supported by DVR player. + */ + public static final int MAX_FAST_FORWARD_SPEED = 256; + + private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); + private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + + private RecordedProgram mProgram; + private long mInitialSeekPositionMs; + private final TvView mTvView; + private DvrPlayerCallback mCallback; + private 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; + private boolean mTimeShiftPlayAvailable; + + public static class DvrPlayerCallback { + /** + * Called when the playback position is changed. The normal updating frequency is + * around 1 sec., which is restricted to the implementation of + * {@link android.media.tv.TvInputService}. + */ + public void onPlaybackPositionChanged(long positionMs) { } + /** + * Called when the playback state or the playback speed is changed. + */ + public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } + /** + * Called when the playback toward the end. + */ + public void onPlaybackEnded() { } + } + + 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 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); + } + + /** + * Prepares playback. + * + * @param doPlay indicates DVR player do or do not start playback after media is prepared. + */ + public void prepare(boolean doPlay) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "prepare()"); + if (mProgram == null) { + throw new IllegalStateException("Recorded program not set"); + } else if (mPlaybackState != PlaybackState.STATE_NONE) { + throw new IllegalStateException("Playback is already prepared"); + } + mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri()); + mPlaybackState = PlaybackState.STATE_CONNECTING; + mPauseOnPrepared = !doPlay; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Resumes playback. + */ + public void play() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "play()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or video not ready yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + break; + default: + mTvView.timeShiftResume(); + } + mPlaybackState = PlaybackState.STATE_PLAYING; + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Pauses playback. + */ + public void pause() throws IllegalStateException { + if (DEBUG) Log.d(TAG, "pause()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + switch (mPlaybackState) { + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + setPlaybackSpeed(1); + // falls through + case PlaybackState.STATE_PLAYING: + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + break; + default: + break; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + + /** + * Fast-forwards playback with the given speed. If the given speed is larger than + * {@value #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}. + */ + public void fastForward(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "fastForward()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_FAST_FORWARD_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(speed); + mPlaybackState = PlaybackState.STATE_FAST_FORWARDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Rewinds playback with the given speed. If the given speed is larger than + * {@value #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}. + */ + public void rewind(int speed) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "rewind()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (speed <= 0) { + throw new IllegalArgumentException("Speed cannot be negative or 0"); + } + if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) { + return; + } + speed = Math.min(speed, MAX_REWIND_SPEED); + if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed); + setPlaybackSpeed(-speed); + mPlaybackState = PlaybackState.STATE_REWINDING; + mCallback.onPlaybackStateChanged(mPlaybackState, speed); + } + + /** + * Seeks playback to the specified position. + */ + public void seekTo(long positionMs) throws IllegalStateException { + if (DEBUG) Log.d(TAG, "seekTo()"); + if (!isPlaybackPrepared()) { + throw new IllegalStateException("Recorded program not set or playback not started yet"); + } + if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) { + return; + } + positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); + if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); + mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); + if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || + mPlaybackState == PlaybackState.STATE_REWINDING) { + mPlaybackState = PlaybackState.STATE_PLAYING; + mTvView.timeShiftResume(); + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } + } + + /** + * Resets playback. + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); + mPlaybackState = PlaybackState.STATE_NONE; + mTvView.reset(); + mTimeShiftPlayAvailable = false; + mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + mTimeShiftCurrentPositionMs = 0; + mPlaybackParams.setSpeed(1.0f); + mProgram = null; + mSelectedAudioTrackId = null; + mSelectedSubtitleTrackId = null; + } + + /** + * Sets callbacks for playback. + */ + public void setCallback(DvrPlayerCallback callback) { + if (callback != null) { + mCallback = callback; + } else { + mCallback = mEmptyCallback; + } + } + + /** + * 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 setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mOnAudioTrackSelectedListener = listener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mOnSubtitleTrackSelectedListener = listener; + } + } + + /** + * Gets the listener to tracks of the given type being selected. + */ + public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mOnAudioTrackSelectedListener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mOnSubtitleTrackSelectedListener; + } + return null; + } + + /** + * Sets recorded programs for playback. If the player is playing another program, stops it. + */ + public void setProgram(RecordedProgram program, long initialSeekPositionMs) { + if (mProgram != null && mProgram.equals(program)) { + return; + } + if (mPlaybackState != PlaybackState.STATE_NONE) { + reset(); + } + mInitialSeekPositionMs = initialSeekPositionMs; + mProgram = program; + } + + /** + * Returns the recorded program now playing. + */ + public RecordedProgram getProgram() { + return mProgram; + } + + /** + * Returns the currrent playback posistion in msecs. + */ + public long getPlaybackPosition() { + return mTimeShiftCurrentPositionMs; + } + + /** + * Returns the playback speed currently used. + */ + public int getPlaybackSpeed() { + return (int) mPlaybackParams.getSpeed(); + } + + /** + * Returns the playback state defined in {@link android.media.session.PlaybackState}. + */ + public int getPlaybackState() { + return mPlaybackState; + } + + /** + * Returns the subtitle tracks of the current playback. + */ + public ArrayList getSubtitleTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); + } + + /** + * Returns the audio tracks of the current playback. + */ + public ArrayList 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() { + return mPlaybackState != PlaybackState.STATE_NONE + && 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 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); + } + + private long getRealSeekPosition(long seekPositionMs, long endMarginMs) { + return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs)); + } + + private void setTvViewCallbacks() { + mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { + @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } + + @Override + public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of playback + // on the first time we get current position, and the start position should be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; + if (mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) { + play(); + } else { + mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); + mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } + } + } + }); + mTvView.setCallback(new TvView.TvInputCallback() { + @Override + public void onTimeShiftStatusChanged(String inputId, int status) { + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); + 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 onTracksChanged(String inputId, List 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); + } + 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 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; + } + } + } + } + } + } + + @Override + public void onContentBlocked(String inputId, TvContentRating rating) { + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); + } + } + }); + } + + private void resumeToWatchedPositionIfNeeded() { + if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, + SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + if (mPauseOnPrepared) { + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + mPauseOnPrepared = false; + } else { + mTvView.timeShiftResume(); + mPlaybackState = PlaybackState.STATE_PLAYING; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } +} \ No newline at end of file diff --git a/src/com/android/tv/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 { - private final T mDefaultValue; + + private static boolean sAllowOverrides = false; + + @VisibleForTesting + public static void initForTest() { + sAllowOverrides = true; + } /** Returns a boolean experiment */ public static ExperimentFlag createFlag( @@ -30,6 +37,11 @@ public final class ExperimentFlag { 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 { /** 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. * - *

- * This file is maintained by hand. + *

This file is maintained by hand. */ public final class Experiments { public static final ExperimentFlag CLOUD_EPG = createFlag( + true); + + /** + * Use network tuner if it is available and there is no other tuner types. + */ + public static final ExperimentFlag 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 { 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 { +public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView { 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 { 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 { 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() { + 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() { + 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() { + 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 { } 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 { } } 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 { 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 { // 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() { + 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() { + 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 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 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 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 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 extends LinearLayout implements ItemListRo if (mTextView != null) { mTextView.setText(resId); } + onTextViewUpdated(); } /** @@ -168,6 +158,22 @@ public abstract class BaseCardView 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 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 { 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 { 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 { 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 { } 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 { 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 @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 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 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); } @@ -159,6 +177,23 @@ public class Menu { mHandler.removeCallbacksAndMessages(null); } + /** + * 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. * 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 customActions) { super(context, menu, R.string.menu_title_options, R.dimen.action_card_height, new TvOptionsRowAdapter(context, customActions)); @@ -90,25 +91,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. */ 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; *

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 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 createActions() { - List 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 customActions) { super(context, customActions); } @@ -53,123 +45,62 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter { protected List createBaseActions() { List 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 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() { + @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 searchChannels(String query, String[] columnForExactMatching, String[] columnForPartialMatching, Set 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 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 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 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() { + @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() { @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 { 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, 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, 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, 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, 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, PsipData.TvTracks return Ints.asList(mProto.audioPids); } - public void setAudioPids(List audioPids) { + synchronized public void setAudioPids(List audioPids) { mProto.audioPids = Ints.toArray(audioPids); } @@ -227,7 +270,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Ints.asList(mProto.audioStreamTypes); } - public void setAudioStreamTypes(List audioStreamTypes) { + synchronized public void setAudioStreamTypes(List audioStreamTypes) { mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); } @@ -239,32 +282,32 @@ public class TunerChannel implements Comparable, 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, 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, PsipData.TvTracks } @Override - public void setHasCaptionTrack() { + synchronized public void setHasCaptionTrack() { mProto.hasCaptionTrack = true; } @@ -312,7 +360,7 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); } - public void setAudioTracks(List audioTracks) { + synchronized public void setAudioTracks(List audioTracks) { mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); } @@ -321,11 +369,11 @@ public class TunerChannel implements Comparable, PsipData.TvTracks return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); } - public void setCaptionTracks(List captionTracks) { + synchronized public void setCaptionTracks(List 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, 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, 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, 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 mTrackFormats; + private int mVideoTrackIndex = INVALID_TRACK_INDEX; + private boolean mVideoTrackMet; + private long mBaseSamplePts = Long.MIN_VALUE; private HashMap mLastExtractedPositionUsMap = new HashMap<>(); + private final List> 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 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 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 trackFormats = new ArrayList<>(); - for (int i = 0; i < trackCount; i++) { - trackFormats.add(mSampleSourceReader.getFormat(i)); - mSampleSourceReader.enable(i, 0); - - } - mTrackFormats = trackFormats; - List 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 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 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> trackInfos = - mBufferManager.readTrackInfoFiles(); - if (trackInfos == null || trackInfos.isEmpty()) { + List 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 ids = new ArrayList<>(); mTrackFormats.clear(); for (int i = 0; i < mTrackCount; ++i) { - Pair 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); @@ -494,6 +496,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen mPlayer.setSelectedTrack(rendererIndex, trackIndex); } + /** + * 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. */ @@ -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/Ac3DefaultTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java new file mode 100644 index 00000000..d442fde8 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer.ac3; + +import android.os.Handler; +import android.os.SystemClock; +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.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; +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; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding. + */ +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; + + // ATSC/53 allows sample rate to be only 48Khz. + // One AC3 sample has 1536 frames, and its duration is 32ms. + public static final long AC3_SAMPLE_DURATION_US = 32000; + + // 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; + + /** + * Interface definition for a callback to be notified of + * {@link com.google.android.exoplayer.audio.AudioTrack} error. + */ + public interface EventListener { + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + void onAudioTrackWriteError(AudioTrack.WriteException e); + } + + private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2; + private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; + private static final int MONITOR_DURATION_MS = 1000; + private static final int AC3_HEADER_BITRATE_OFFSET = 4; + + // Keep this as static in order to prevent new framework AudioTrack creation + // while old AudioTrack is being released. + private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper(); + private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000; + + // Ignore AudioTrack backward movement if duration of movement is below the threshold. + private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000; + + // AudioTrack position cannot go ahead beyond this limit. + private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000; + + // Since MediaCodec processing and AudioTrack playing add delay, + // PTS interpolated time should be delayed reasonably when AudioTrack is not used. + private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; + + private final CodecCounters mCodecCounters; + private final SampleSource.SampleSourceReader mSource; + private final SampleHolder mSampleHolder; + private final MediaFormatHolder mFormatHolder; + private final EventListener mEventListener; + private final Handler mEventHandler; + private final AudioTrackMonitor mMonitor; + private final AudioClock mAudioClock; + + private MediaFormat mFormat; + private boolean mFormatConfigured; + private int mSampleSize; + private final ByteBuffer mOutputBuffer; + private boolean mOutputReady; + private int mTrackIndex; + private boolean mSourceStateReady; + private boolean mInputStreamEnded; + private boolean mOutputStreamEnded; + private long mEndOfStreamMs; + private long mCurrentPositionUs; + private int mPresentationCount; + private long mPresentationTimeUs; + private long mInterpolatedTimeUs; + private long mPreviousPositionUs; + private boolean mIsStopped; + private boolean mEnabled = true; + private boolean mIsMuted; + private ArrayList mTracksIndex; + + public Ac3DefaultTrackRenderer( + SampleSource source, + Handler eventHandler, + EventListener listener, + boolean usePassthrough) { + mSource = source.register(); + mEventHandler = eventHandler; + mEventListener = listener; + mTrackIndex = -1; + mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); + mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); + mFormatHolder = new MediaFormatHolder(); + AUDIO_TRACK.restart(); + mCodecCounters = new CodecCounters(); + mMonitor = new AudioTrackMonitor(); + mAudioClock = new AudioClock(); + mTracksIndex = new ArrayList<>(); + } + + @Override + protected MediaClock getMediaClock() { + return this; + } + + private static boolean handlesMimeType(String mimeType) { + return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3); + } + + @Override + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean sourcePrepared = mSource.prepare(positionUs); + if (!sourcePrepared) { + return false; + } + for (int i = 0; i < mSource.getTrackCount(); i++) { + if (handlesMimeType(mSource.getFormat(i).mimeType)) { + if (mTrackIndex < 0) { + mTrackIndex = i; + } + mTracksIndex.add(i); + } + } + + // TODO: Check this case. Source does not have the proper mime type. + return true; + } + + @Override + protected int getTrackCount() { + return mTracksIndex.size(); + } + + @Override + protected MediaFormat getFormat(int track) { + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + return mSource.getFormat(mTracksIndex.get(track)); + } + + @Override + protected void onEnabled(int track, long positionUs, boolean joining) { + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + mTrackIndex = mTracksIndex.get(track); + mSource.enable(mTrackIndex, positionUs); + seekToInternal(positionUs); + } + + @Override + protected void onDisabled() { + AUDIO_TRACK.resetSessionId(); + clearDecodeState(); + mFormat = null; + mSource.disable(mTrackIndex); + } + + @Override + protected void onReleased() { + AUDIO_TRACK.release(); + mSource.release(); + } + + @Override + protected boolean isEnded() { + return mOutputStreamEnded && AUDIO_TRACK.isEnded(); + } + + @Override + protected boolean isReady() { + return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady)); + } + + private void seekToInternal(long positionUs) { + mMonitor.reset(MONITOR_DURATION_MS); + mSourceStateReady = false; + mInputStreamEnded = false; + mOutputStreamEnded = false; + mPresentationTimeUs = positionUs; + mPresentationCount = 0; + mPreviousPositionUs = 0; + mCurrentPositionUs = Long.MIN_VALUE; + mInterpolatedTimeUs = Long.MIN_VALUE; + mAudioClock.setPositionUs(positionUs); + } + + @Override + protected void seekTo(long positionUs) { + mSource.seekToUs(positionUs); + AUDIO_TRACK.reset(); + // resetSessionId() will create a new framework AudioTrack instead of reusing old one. + AUDIO_TRACK.resetSessionId(); + seekToInternal(positionUs); + } + + @Override + protected void onStarted() { + AUDIO_TRACK.play(); + mAudioClock.start(); + mIsStopped = false; + } + + @Override + protected void onStopped() { + AUDIO_TRACK.pause(); + mAudioClock.stop(); + mIsStopped = true; + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + try { + mSource.maybeThrowError(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + mMonitor.maybeLog(); + try { + if (mEndOfStreamMs != 0) { + // Ensure playback stops, after EoS was notified. + // Sometimes MediaCodecTrackRenderer does not fetch EoS timely + // after EoS was notified here long before. + long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs; + if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) { + throw new ExoPlaybackException("Much time has elapsed after EoS"); + } + } + boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); + if (mSourceStateReady != continueBuffering) { + mSourceStateReady = continueBuffering; + if (DEBUG) { + Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady)); + } + } + long discontinuity = mSource.readDiscontinuity(mTrackIndex); + if (discontinuity != SampleSource.NO_DISCONTINUITY) { + AUDIO_TRACK.handleDiscontinuity(); + mPresentationTimeUs = discontinuity; + mPresentationCount = 0; + clearDecodeState(); + return; + } + if (mFormat == null) { + readFormat(); + return; + } + + // Process only one sample at a time for doSomeWork() + if (processOutput()) { + if (!mOutputReady) { + while (feedInputBuffer()) { + if (mOutputReady) break; + } + } + } + mCodecCounters.ensureUpdated(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + private void ensureAudioTrackInitialized() { + if (!AUDIO_TRACK.isInitialized()) { + try { + if (DEBUG) { + Log.d(TAG, "AudioTrack initialized"); + } + AUDIO_TRACK.initialize(); + } catch (AudioTrack.InitializationException e) { + Log.e(TAG, "Error on AudioTrack initialization", e); + notifyAudioTrackInitializationError(e); + + // Do not throw exception here but just disabling audioTrack to keep playing + // video without audio. + AUDIO_TRACK.setStatus(false); + } + if (getState() == TrackRenderer.STATE_STARTED) { + if (DEBUG) { + Log.d(TAG, "AudioTrack played"); + } + AUDIO_TRACK.play(); + } + } + } + + private void clearDecodeState() { + mOutputReady = false; + AUDIO_TRACK.reset(); + } + + private void readFormat() throws IOException, ExoPlaybackException { + int result = mSource.readData(mTrackIndex, mCurrentPositionUs, + mFormatHolder, mSampleHolder); + if (result == SampleSource.FORMAT_READ) { + onInputFormatChanged(mFormatHolder); + } + } + + 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; + mFormatConfigured = true; + if (DEBUG) { + Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); + } + clearDecodeState(); + 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 { + if (mInputStreamEnded) { + return false; + } + + mSampleHolder.data.clear(); + mSampleHolder.size = 0; + int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, + mSampleHolder); + switch (result) { + case SampleSource.NOTHING_READ: { + return false; + } + case SampleSource.FORMAT_READ: { + Log.i(TAG, "Format was read again"); + onInputFormatChanged(mFormatHolder); + return true; + } + case SampleSource.END_OF_STREAM: { + Log.i(TAG, "End of stream from SampleSource"); + mInputStreamEnded = true; + return false; + } + default: { + if (mSampleHolder.size != mSampleSize && mFormatConfigured) { + onSampleSizeChanged(mSampleHolder.size); + } + mSampleHolder.data.flip(); + decodeDone(mSampleHolder.data, mSampleHolder.timeUs); + return true; + } + } + } + + private boolean processOutput() throws ExoPlaybackException { + if (mOutputStreamEnded) { + return false; + } + if (!mOutputReady) { + if (mInputStreamEnded) { + mOutputStreamEnded = true; + mEndOfStreamMs = SystemClock.elapsedRealtime(); + return false; + } + return true; + } + + ensureAudioTrackInitialized(); + int handleBufferResult; + try { + // To reduce discontinuity, interpolate presentation time. + mInterpolatedTimeUs = mPresentationTimeUs + + mPresentationCount * AC3_SAMPLE_DURATION_US; + handleBufferResult = AUDIO_TRACK.handleBuffer(mOutputBuffer, + 0, mOutputBuffer.limit(), mInterpolatedTimeUs); + } catch (AudioTrack.WriteException e) { + notifyAudioTrackWriteError(e); + throw new ExoPlaybackException(e); + } + + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + Log.i(TAG, "Play discontinuity happened"); + mCurrentPositionUs = Long.MIN_VALUE; + } + if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { + mCodecCounters.renderedOutputBufferCount++; + mOutputReady = false; + return true; + } + return false; + } + + @Override + protected long getDurationUs() { + return mSource.getFormat(mTrackIndex).durationUs; + } + + @Override + protected long getBufferedPositionUs() { + long pos = mSource.getBufferedPositionUs(); + return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US + ? pos : Math.max(pos, getPositionUs()); + } + + @Override + public long getPositionUs() { + if (!AUDIO_TRACK.isInitialized()) { + return mAudioClock.getPositionUs(); + } else if (!AUDIO_TRACK.isEnabled()) { + if (mInterpolatedTimeUs > 0) { + return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; + } + return mPresentationTimeUs; + } + long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded()); + if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { + mPreviousPositionUs = 0L; + if (DEBUG) { + long oldPositionUs = Math.max(mCurrentPositionUs, 0); + long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); + Log.d(TAG, "Audio position is not set, diff in us: " + + String.valueOf(currentPositionUs - oldPositionUs)); + } + mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); + } else { + if (mPreviousPositionUs + > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) { + Log.e(TAG, "audio_position BACK JUMP: " + + (mPreviousPositionUs - audioTrackCurrentPositionUs)); + mCurrentPositionUs = audioTrackCurrentPositionUs; + } else { + mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs); + } + mPreviousPositionUs = audioTrackCurrentPositionUs; + } + long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US; + if (mCurrentPositionUs > upperBound) { + mCurrentPositionUs = upperBound; + } + return mCurrentPositionUs; + } + + private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) { + if (outputBuffer == null || mOutputBuffer == null) { + return; + } + if (presentationTimeUs < 0) { + Log.e(TAG, "decodeDone - invalid presentationTimeUs"); + return; + } + + if (TunerDebug.ENABLED) { + TunerDebug.setAudioPtsUs(presentationTimeUs); + } + + mOutputBuffer.clear(); + Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); + + mOutputBuffer.put(outputBuffer); + mMonitor.addPts(presentationTimeUs, mOutputBuffer.position(), + mOutputBuffer.get(AC3_HEADER_BITRATE_OFFSET)); + if (presentationTimeUs == mPresentationTimeUs) { + mPresentationCount++; + } else { + mPresentationCount = 0; + mPresentationTimeUs = presentationTimeUs; + } + mOutputBuffer.flip(); + mOutputReady = true; + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (mEventHandler == null || mEventListener == null) { + return; + } + mEventHandler.post(new Runnable() { + @Override + public void run() { + mEventListener.onAudioTrackInitializationError(e); + } + }); + } + + private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { + if (mEventHandler == null || mEventListener == null) { + return; + } + mEventHandler.post(new Runnable() { + @Override + public void run() { + mEventListener.onAudioTrackWriteError(e); + } + }); + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case MSG_SET_VOLUME: + 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: + mEnabled = (Integer) message == 1; + setStatus(mEnabled); + break; + case MSG_SET_PLAYBACK_SPEED: + mAudioClock.setPlaybackSpeed((Float) message); + break; + default: + 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/Ac3MediaCodecTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java new file mode 100644 index 00000000..604959d1 --- /dev/null +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.exoplayer.ac3; + +import android.os.Handler; + +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.SampleSource; + +/** + * MPEG-2 TS audio track renderer. + * + *

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 Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer { + private final String TAG = "Ac3MediaCodecTrackRenderer"; + private final boolean DEBUG = false; + + private final Ac3EventListener mListener; + + public interface Ac3EventListener extends EventListener { + /** + * Invoked when a {@link android.media.PlaybackParams} set to an + * {@link android.media.AudioTrack} is not valid. + * + * @param e The corresponding exception. + */ + void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); + } + + public Ac3MediaCodecTrackRenderer( + SampleSource source, + MediaCodecSelector mediaCodecSelector, + Handler eventHandler, + EventListener eventListener) { + super(source, mediaCodecSelector, eventHandler, eventListener); + mListener = (Ac3EventListener) eventListener; + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_PLAYBACK_PARAMS) { + try { + super.handleMessage(messageType, message); + } catch (IllegalArgumentException e) { + if (isAudioTrackSetPlaybackParamsError(e)) { + notifyAudioTrackSetPlaybackParamsError(e); + } + } + return; + } + super.handleMessage(messageType, message); + } + + private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { + if (eventHandler != null && mListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + mListener.onAudioTrackSetPlaybackParamsError(e); + } + }); + } + } + + static private boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { + if (e.getStackTrace() == null || e.getStackTrace().length < 1) { + return false; + } + for (StackTraceElement element : e.getStackTrace()) { + String elementString = element.toString(); + if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java deleted file mode 100644 index 9dae2e34..00000000 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.ac3; - -import android.os.Handler; -import android.os.SystemClock; -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.android.tv.tuner.tvinput.TunerDebug; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** - * Decodes and renders AC3 audio. - */ -public class Ac3PassthroughTrackRenderer 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; - - // ATSC/53 allows sample rate to be only 48Khz. - // 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"; - private static final boolean DEBUG = false; - - /** - * Interface definition for a callback to be notified of - * {@link com.google.android.exoplayer.audio.AudioTrack} error. - */ - public interface EventListener { - void onAudioTrackInitializationError(AudioTrack.InitializationException e); - void onAudioTrackWriteError(AudioTrack.WriteException e); - } - - private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2; - private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; - private static final int MONITOR_DURATION_MS = 1000; - private static final int AC3_HEADER_BITRATE_OFFSET = 4; - - // Keep this as static in order to prevent new framework AudioTrack creation - // while old AudioTrack is being released. - private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper(); - private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000; - - // Ignore AudioTrack backward movement if duration of movement is below the threshold. - private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000; - - // AudioTrack position cannot go ahead beyond this limit. - private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000; - - // Since MediaCodec processing and AudioTrack playing add delay, - // PTS interpolated time should be delayed reasonably when AudioTrack is not used. - private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; - - private final CodecCounters mCodecCounters; - private final SampleSource.SampleSourceReader mSource; - private final SampleHolder mSampleHolder; - private final MediaFormatHolder mFormatHolder; - private final EventListener mEventListener; - private final Handler mEventHandler; - private final AudioTrackMonitor mMonitor; - private final AudioClock mAudioClock; - - private MediaFormat mFormat; - private final ByteBuffer mOutputBuffer; - private boolean mOutputReady; - private int mTrackIndex; - private boolean mSourceStateReady; - private boolean mInputStreamEnded; - private boolean mOutputStreamEnded; - private long mEndOfStreamMs; - private long mCurrentPositionUs; - private int mPresentationCount; - private long mPresentationTimeUs; - private long mInterpolatedTimeUs; - private long mPreviousPositionUs; - private boolean mIsStopped; - private ArrayList mTracksIndex; - - public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler, - EventListener listener) { - mSource = source.register(); - mEventHandler = eventHandler; - mEventListener = listener; - mTrackIndex = -1; - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); - mFormatHolder = new MediaFormatHolder(); - AUDIO_TRACK.restart(); - mCodecCounters = new CodecCounters(); - mMonitor = new AudioTrackMonitor(); - mAudioClock = new AudioClock(); - mTracksIndex = new ArrayList<>(); - } - - @Override - protected MediaClock getMediaClock() { - return this; - } - - private static boolean handlesMimeType(String mimeType) { - return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3); - } - - @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return false; - } - for (int i = 0; i < mSource.getTrackCount(); i++) { - if (handlesMimeType(mSource.getFormat(i).mimeType)) { - if (mTrackIndex < 0) { - mTrackIndex = i; - } - mTracksIndex.add(i); - } - } - - // TODO: Check this case. Source does not have the proper mime type. - return true; - } - - @Override - protected int getTrackCount() { - return mTracksIndex.size(); - } - - @Override - protected MediaFormat getFormat(int track) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - return mSource.getFormat(mTracksIndex.get(track)); - } - - @Override - protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - mTrackIndex = mTracksIndex.get(track); - mSource.enable(mTrackIndex, positionUs); - seekToInternal(positionUs); - } - - @Override - protected void onDisabled() { - AUDIO_TRACK.resetSessionId(); - clearDecodeState(); - mFormat = null; - mSource.disable(mTrackIndex); - } - - @Override - protected void onReleased() { - AUDIO_TRACK.release(); - mSource.release(); - } - - @Override - protected boolean isEnded() { - return mOutputStreamEnded && AUDIO_TRACK.isEnded(); - } - - @Override - protected boolean isReady() { - return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady)); - } - - private void seekToInternal(long positionUs) { - mMonitor.reset(MONITOR_DURATION_MS); - mSourceStateReady = false; - mInputStreamEnded = false; - mOutputStreamEnded = false; - mPresentationTimeUs = positionUs; - mPresentationCount = 0; - mPreviousPositionUs = 0; - mCurrentPositionUs = Long.MIN_VALUE; - mInterpolatedTimeUs = Long.MIN_VALUE; - mAudioClock.setPositionUs(positionUs); - } - - @Override - protected void seekTo(long positionUs) { - mSource.seekToUs(positionUs); - AUDIO_TRACK.reset(); - // resetSessionId() will create a new framework AudioTrack instead of reusing old one. - AUDIO_TRACK.resetSessionId(); - seekToInternal(positionUs); - } - - @Override - protected void onStarted() { - AUDIO_TRACK.play(); - mAudioClock.start(); - mIsStopped = false; - } - - @Override - protected void onStopped() { - AUDIO_TRACK.pause(); - mAudioClock.stop(); - mIsStopped = true; - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - try { - mSource.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - mMonitor.maybeLog(); - try { - if (mEndOfStreamMs != 0) { - // Ensure playback stops, after EoS was notified. - // Sometimes MediaCodecTrackRenderer does not fetch EoS timely - // after EoS was notified here long before. - long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs; - if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) { - throw new ExoPlaybackException("Much time has elapsed after EoS"); - } - } - boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); - if (mSourceStateReady != continueBuffering) { - mSourceStateReady = continueBuffering; - if (DEBUG) { - Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady)); - } - } - long discontinuity = mSource.readDiscontinuity(mTrackIndex); - if (discontinuity != SampleSource.NO_DISCONTINUITY) { - AUDIO_TRACK.handleDiscontinuity(); - mPresentationTimeUs = discontinuity; - mPresentationCount = 0; - clearDecodeState(); - return; - } - if (mFormat == null) { - readFormat(); - return; - } - - // Process only one sample at a time for doSomeWork() - if (processOutput()) { - if (!mOutputReady) { - while (feedInputBuffer()) { - if (mOutputReady) break; - } - } - } - mCodecCounters.ensureUpdated(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - private void ensureAudioTrackInitialized() { - if (!AUDIO_TRACK.isInitialized()) { - try { - if (DEBUG) { - Log.d(TAG, "AudioTrack initialized"); - } - AUDIO_TRACK.initialize(); - } catch (AudioTrack.InitializationException e) { - Log.e(TAG, "Error on AudioTrack initialization", e); - notifyAudioTrackInitializationError(e); - - // Do not throw exception here but just disabling audioTrack to keep playing - // video without audio. - AUDIO_TRACK.setStatus(false); - } - if (getState() == TrackRenderer.STATE_STARTED) { - if (DEBUG) { - Log.d(TAG, "AudioTrack played"); - } - AUDIO_TRACK.play(); - } - } - } - - private void clearDecodeState() { - mOutputReady = false; - AUDIO_TRACK.reset(); - } - - private void readFormat() throws IOException, ExoPlaybackException { - int result = mSource.readData(mTrackIndex, mCurrentPositionUs, - mFormatHolder, mSampleHolder); - if (result == SampleSource.FORMAT_READ) { - onInputFormatChanged(mFormatHolder); - } - } - - private void onInputFormatChanged(MediaFormatHolder formatHolder) - throws ExoPlaybackException { - mFormat = formatHolder.format; - if (DEBUG) { - Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); - } - clearDecodeState(); - AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16()); - } - - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { - if (mInputStreamEnded) { - return false; - } - - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, - mSampleHolder); - switch (result) { - case SampleSource.NOTHING_READ: { - return false; - } - case SampleSource.FORMAT_READ: { - Log.i(TAG, "Format was read again"); - onInputFormatChanged(mFormatHolder); - return true; - } - case SampleSource.END_OF_STREAM: { - Log.i(TAG, "End of stream from SampleSource"); - mInputStreamEnded = true; - return false; - } - default: { - mSampleHolder.data.flip(); - decodeDone(mSampleHolder.data, mSampleHolder.timeUs); - return true; - } - } - } - - private boolean processOutput() throws ExoPlaybackException { - if (mOutputStreamEnded) { - return false; - } - if (!mOutputReady) { - if (mInputStreamEnded) { - mOutputStreamEnded = true; - mEndOfStreamMs = SystemClock.elapsedRealtime(); - return false; - } - return true; - } - - ensureAudioTrackInitialized(); - int handleBufferResult; - try { - // To reduce discontinuity, interpolate presentation time. - mInterpolatedTimeUs = mPresentationTimeUs - + mPresentationCount * AC3_SAMPLE_DURATION_US; - handleBufferResult = AUDIO_TRACK.handleBuffer(mOutputBuffer, - 0, mOutputBuffer.limit(), mInterpolatedTimeUs); - } catch (AudioTrack.WriteException e) { - notifyAudioTrackWriteError(e); - throw new ExoPlaybackException(e); - } - - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - Log.i(TAG, "Play discontinuity happened"); - mCurrentPositionUs = Long.MIN_VALUE; - } - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { - mCodecCounters.renderedOutputBufferCount++; - mOutputReady = false; - return true; - } - return false; - } - - @Override - protected long getDurationUs() { - return mSource.getFormat(mTrackIndex).durationUs; - } - - @Override - protected long getBufferedPositionUs() { - long pos = mSource.getBufferedPositionUs(); - return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US - ? pos : Math.max(pos, getPositionUs()); - } - - @Override - public long getPositionUs() { - if (!AUDIO_TRACK.isInitialized()) { - return mAudioClock.getPositionUs(); - } else if (!AUDIO_TRACK.isEnabled()) { - if (mInterpolatedTimeUs > 0) { - return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; - } - return mPresentationTimeUs; - } - long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded()); - if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { - mPreviousPositionUs = 0L; - if (DEBUG) { - long oldPositionUs = Math.max(mCurrentPositionUs, 0); - long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - Log.d(TAG, "Audio position is not set, diff in us: " - + String.valueOf(currentPositionUs - oldPositionUs)); - } - mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - } else { - if (mPreviousPositionUs - > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) { - Log.e(TAG, "audio_position BACK JUMP: " - + (mPreviousPositionUs - audioTrackCurrentPositionUs)); - mCurrentPositionUs = audioTrackCurrentPositionUs; - } else { - mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs); - } - mPreviousPositionUs = audioTrackCurrentPositionUs; - } - long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US; - if (mCurrentPositionUs > upperBound) { - mCurrentPositionUs = upperBound; - } - return mCurrentPositionUs; - } - - private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) { - if (outputBuffer == null || mOutputBuffer == null) { - return; - } - if (presentationTimeUs < 0) { - Log.e(TAG, "decodeDone - invalid presentationTimeUs"); - return; - } - - if (TunerDebug.ENABLED) { - TunerDebug.setAudioPtsUs(presentationTimeUs); - } - - mOutputBuffer.clear(); - Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); - - mOutputBuffer.put(outputBuffer); - mMonitor.addPts(presentationTimeUs, mOutputBuffer.position(), - mOutputBuffer.get(AC3_HEADER_BITRATE_OFFSET)); - if (presentationTimeUs == mPresentationTimeUs) { - mPresentationCount++; - } else { - mPresentationCount = 0; - mPresentationTimeUs = presentationTimeUs; - } - mOutputBuffer.flip(); - mOutputReady = true; - } - - private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { - if (mEventHandler == null || mEventListener == null) { - return; - } - mEventHandler.post(new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackInitializationError(e); - } - }); - } - - private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { - if (mEventHandler == null || mEventListener == null) { - return; - } - mEventHandler.post(new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackWriteError(e); - } - }); - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_SET_VOLUME: - AUDIO_TRACK.setVolume((Float) message); - 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()); - } - break; - case MSG_SET_PLAYBACK_SPEED: - mAudioClock.setPlaybackSpeed((Float) message); - break; - default: - super.handleMessage(messageType, message); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java deleted file mode 100644 index 2bf86b5a..00000000 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.ac3; - -import android.os.Handler; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.SampleSource; - -/** - * MPEG-2 TS audio track renderer. - *

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"; - private final boolean DEBUG = false; - - private final Ac3EventListener mListener; - - public interface Ac3EventListener extends EventListener { - /** - * Invoked when a {@link android.media.PlaybackParams} set to an - * {@link android.media.AudioTrack} is not valid. - * - * @param e The corresponding exception. - */ - void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); - } - - public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector, - Handler eventHandler, EventListener eventListener) { - super(source, mediaCodecSelector, eventHandler, eventListener); - mListener = (Ac3EventListener) eventListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_PLAYBACK_PARAMS) { - try { - super.handleMessage(messageType, message); - } catch (IllegalArgumentException e) { - if (isAudioTrackSetPlaybackParamsError(e)) { - notifyAudioTrackSetPlaybackParamsError(e); - } - } - return; - } - super.handleMessage(messageType, message); - } - - private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { - if (eventHandler != null && mListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - mListener.onAudioTrackSetPlaybackParamsError(e); - } - }); - } - } - - static private boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (e.getStackTrace() == null || e.getStackTrace().length < 1) { - return false; - } - for (StackTraceElement element : e.getStackTrace()) { - String elementString = element.toString(); - if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) { - return true; - } - } - return false; - } -} \ No newline at end of file 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> mChunkMap = new ArrayMap<>(); + private final Map>> mChunkMap = + new ArrayMap<>(); private final Map mStartPositionMap = new ArrayMap<>(); private final Map 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); @@ -173,6 +173,66 @@ public class BufferManager { void release() throws IOException; } + /** + * 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} */ @@ -185,11 +245,6 @@ public class BufferManager { */ File getBufferDir(); - /** - * Cleans up storage. - */ - void clearStorage(); - /** * Informs whether the storage is used for persistent use. (eg. dvr recording/play) * @@ -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 readTrackInfoFile(boolean isAudio) throws IOException; + List 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 readIndexFile(String trackId) throws IOException; + ArrayList 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 formatList, boolean isAudio) throws IOException; /** @@ -252,7 +305,7 @@ public class BufferManager { * @param index {@link SampleChunk} container * @throws IOException */ - void writeIndexFile(String trackName, SortedMap index) + void writeIndexFile(String trackName, SortedMap> 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 map = mChunkMap.get(id); + SortedMap> 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 keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0; + ArrayList keyPositions = mStorageManager.readIndexFile(trackId); + long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; - SortedMap map = mChunkMap.get(trackId); + SortedMap> 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 map = mChunkMap.get(id); + public Pair getReadFile(String id, long positionUs) { + SortedMap> map = mChunkMap.get(id); if (map == null) { return null; } - SampleChunk sampleChunk; - SortedMap headMap = map.headMap(positionUs + 1); + Pair ret; + SortedMap> 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 earliestChunkMap = null; + SortedMap> earliestChunkMap = null; SampleChunk earliestChunk = null; String earliestChunkId = null; - for (Map.Entry> entry : mChunkMap.entrySet()) { - SortedMap map = entry.getValue(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SortedMap> 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> entry : mChunkMap.entrySet()) { - SortedMap map = entry.getValue(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SortedMap> 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> readTrackInfoFiles() throws IOException { - ArrayList> 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 readTrackInfoFiles() throws IOException { + List 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 audio, Pair video) + public void writeMetaFiles(List audios, List videos) throws IOException { - if (audio != null) { - mStorageManager.writeTrackInfoFile(audio.first, audio.second, true); - SortedMap 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> 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 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> 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> entry : mChunkMap.entrySet()) { - for (SampleChunk chunk : entry.getValue().values()) { - SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()); + try { + mPendingDelete.release(); + for (Map.Entry>> entry : + mChunkMap.entrySet()) { + SampleChunk toRelease = null; + for (Pair 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()); } } @@ -610,20 +672,6 @@ public class BufferManager { return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); } - /** - * 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. @@ -58,18 +69,6 @@ public class DvrStorageManager implements BufferManager.StorageManager { mIsRecording = isRecording; } - @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 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 readTrackInfoFiles(boolean isAudio) { + List 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 readCaptionInfoFiles() { + List 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 readOldIndexFile(File indexFile) + throws IOException { + ArrayList 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 readIndexFile(String trackId) throws IOException { - ArrayList indices = new ArrayList<>(); - File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX); - try (DataInputStream in = new DataInputStream(new FileInputStream(file))) { + private ArrayList readNewIndexFile(File indexFile) + throws IOException { + ArrayList 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 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 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 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 index) + public void writeIndexFile(String trackName, SortedMap> 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> 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 mIds; private List 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); } /** @@ -240,6 +245,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. * 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 mIds; private final List mMediaFormats; @@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback { private Handler mIoHandler; private final ConcurrentLinkedQueue mReadSampleBuffers[]; private final ConcurrentLinkedQueue 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 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(); } @@ -203,6 +219,15 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_READ, params)); } + /** + * 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. */ @@ -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 audio = null, video = null; + List audios = new LinkedList<>(); + List 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 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 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() { - @Override - protected Void doInBackground(Void... params) { - for (File file : files) { + sLastCacheCleanUpTask = new AsyncTask() { + @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 readTrackInfoFile(boolean isAudio) { + public List readTrackInfoFiles(boolean isAudio) { return null; } @Override - public ArrayList readIndexFile(String trackId) { + public ArrayList readIndexFile(String trackId) { return null; } @Override - public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) { + public void writeTrackInfoFiles(List formatList, boolean isAudio) { } @Override - public void writeIndexFile(String trackName, SortedMap index) { + public void writeIndexFile(String trackName, + SortedMap> 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 @@ -35,6 +35,24 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment { public static final String ACTION_CATEGORY = "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 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); @@ -223,6 +293,27 @@ public class TunerSetupActivity extends SetupActivity { return intent; } + /** + * 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. * @@ -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 { + @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; @@ -126,6 +128,14 @@ public class TsDataSourceManager { mKeepTuneStatus = keepTuneStatus; } + /** + * Add tuner hal into TunerTsStreamerManager for test. + */ + @VisibleForTesting + public void addTunerHalForTest(TunerHal tunerHal) { + mTunerStreamerManager.addTunerHal(tunerHal, mId); + } + /** * Releases persistent resources. */ 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 mCreators = new HashMap<>(); + private final Map mListeners = new HashMap<>(); private final Map 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 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 mChannelMap = new SparseArray<>(); private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final EventListener mEventListener; + private final List 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 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 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> pair = + (Pair>) 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 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 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 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 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 audioTracks = tvTracksInterface.getAudioTracks(); - List 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 audioTracks = tvTracksInterface.getAudioTracks(); + List 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/StringUtils.java b/src/com/android/tv/tuner/util/StringUtils.java deleted file mode 100644 index 15571e75..00000000 --- a/src/com/android/tv/tuner/util/StringUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -/** - * Utility class for handling {@link String}. - */ -public final class StringUtils { - - private StringUtils() { } - - /** - * Returns compares two strings lexicographically and handles null values quietly. - */ - public static int compare(String a, String b) { - if (a == null) { - return b == null ? 0 : -1; - } - if (b == null) { - return 1; - } - return a.compareTo(b); - } -} 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 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 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 { - @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) { @@ -714,6 +791,11 @@ public class TunableTvView extends FrameLayout implements StreamInfo { return mVideoAvailable; } + @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 createProgramPosterArtCallback( + TuningBlockView view, final long channelId) { + return new ImageLoader.ImageLoaderCallback(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 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); } @@ -563,6 +567,14 @@ public class TvOverlayManager { new RecentlyWatchedDialogFragment(), false); } + /** + * Shows DVR history dialog. + */ + public void showDvrHistoryDialog() { + showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG, + new DvrHistoryDialogFragment(), false); + } + /** * Shows banner view. */ @@ -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); } @@ -326,120 +297,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. */ @@ -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 getItemList() { List 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) { @@ -34,6 +35,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. */ @@ -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 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 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 { @@ -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/SimpleActionItem.java b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java new file mode 100644 index 00000000..42553b66 --- /dev/null +++ b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java @@ -0,0 +1,34 @@ +/* + * 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.sidepanel; + +/** + * A simple item which shows title and description. + */ +public class SimpleActionItem extends ActionItem { + public SimpleActionItem(String title) { + super(title); + } + + public SimpleActionItem(String title, String description) { + super(title, description); + } + + @Override + protected void onSelected() { + } +} diff --git a/src/com/android/tv/ui/sidepanel/SimpleItem.java b/src/com/android/tv/ui/sidepanel/SimpleItem.java deleted file mode 100644 index 52a5f13f..00000000 --- a/src/com/android/tv/ui/sidepanel/SimpleItem.java +++ /dev/null @@ -1,34 +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.ui.sidepanel; - -/** - * A simple item which shows title and description. - */ -public class SimpleItem extends ActionItem { - public SimpleItem(String title) { - super(title); - } - - public SimpleItem(String title, String description) { - super(title, description); - } - - @Override - protected void onSelected() { - } -} 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 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/util/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java new file mode 100644 index 00000000..b6221496 --- /dev/null +++ b/src/com/android/tv/util/DurationTimer.java @@ -0,0 +1,84 @@ +/* + * 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.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. + */ + public boolean isRunning() { + return startTimeMs != TIME_NOT_SET; + } + + /** + * Start the timer. + */ + public void start() { + startTimeMs = SystemClock.elapsedRealtime(); + } + + /** + * Returns the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long getDuration() { + return isRunning() ? SystemClock.elapsedRealtime() - startTimeMs : TIME_NOT_SET; + } + + /** + * Stops the timer and resets its value to {@link #TIME_NOT_SET}. + * + * @return the current duration in milliseconds or {@link #TIME_NOT_SET} if the timer is not + * running. + */ + public long reset() { + long duration = getDuration(); + 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

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 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 getInputsOrderMap() { + HashMap 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 mPipInputMap = new HashMap<>(); // inputId -> PipInput - private final Set 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. - * - *

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.. - * - *

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 getPipInputList(boolean availableOnly) { - List pipInputs = new ArrayList<>(); - List 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() { - @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() { @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/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java new file mode 100644 index 00000000..659807e2 --- /dev/null +++ b/src/com/android/tv/util/StringUtils.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * Utility class for handling {@link String}. + */ +public final class StringUtils { + + private StringUtils() { } + + /** + * Returns compares two strings lexicographically and handles null values quietly. + */ + public static int compare(String a, String b) { + if (a == null) { + return b == null ? 0 : -1; + } + if (b == null) { + return 1; + } + return a.compareTo(b); + } +} 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 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 mInputStateMap = new HashMap<>(); private final Map mInputMap = new HashMap<>(); + private final Map mTvInputLabels = new ArrayMap<>(); + private final Map mTvInputCustomLabels = new ArrayMap<>(); private final Map mInputIdToPartnerInputMap = new HashMap<>(); + + private final Map mTvInputApplicationLabels = new ArrayMap<>(); + private final Map mTvInputApplicationIcons = new ArrayMap<>(); + private final Map 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 getTvInputInfos(boolean availableOnly, boolean tunerOnly) { ArrayList list = new ArrayList<>(); for (Map.Entry 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 { + private Map 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 ids) { Set 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. @@ -206,6 +208,28 @@ public class Utils { .apply(); } + /** + * Adds the info of failed scheduled recording. + */ + public static void addFailedScheduledRecordingInfo(Context context, + String scheduledRecordingInfo) { + Set 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. */ @@ -245,6 +269,14 @@ public class Utils { RECORDING_FAILED_REASON_NONE); } + /** + * Returns the failed scheduled recordings info set. + */ + public static Set getFailedScheduledRecordingInfoSet(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context) + .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>()); + } + /** * Checks do recording failed reason exist. */ @@ -332,6 +364,14 @@ public class Utils { return getProgramAt(context, channelId, System.currentTimeMillis()); } + /** + * 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, @@ -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> 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 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 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(); + } +} -- cgit v1.2.3