aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2017-10-03 10:16:37 -0700
committerNick Chalko <nchalko@google.com>2017-10-04 13:48:13 +0000
commit6ebde20b03db4c0d57f67acaac11832b610b966b (patch)
treed31e2adc1f9cce4f27ca07d30bee921032e33a3c /src
parentee027a576ddebaf1ae739219be01b0240b15f80c (diff)
downloadTV-6ebde20b03db4c0d57f67acaac11832b610b966b.tar.gz
Sync to match Live Channels 1.15(ncis)oreo-mr1-dev
aka ub-tv-dev at a73a150bb7d0d1ce867ef980c6ac8411899d40ad Bug: 64021596 Change-Id: I7c544fd15e2c58784f8babc31877ad0dfeebb4c0 (cherry picked from commit 633eb826b8c97731dfc5ef12c7bf78a63734275d)
Diffstat (limited to 'src')
-rw-r--r--src/com/android/exoplayer/text/SubtitleView.java4
-rw-r--r--src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java127
-rw-r--r--src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java84
-rw-r--r--src/com/android/tv/ApplicationSingletons.java27
-rw-r--r--src/com/android/tv/AudioManagerHelper.java108
-rw-r--r--src/com/android/tv/Features.java151
-rw-r--r--src/com/android/tv/InputSessionManager.java30
-rw-r--r--src/com/android/tv/MainActivity.java1498
-rw-r--r--src/com/android/tv/MainActivityWrapper.java2
-rw-r--r--src/com/android/tv/MediaSessionWrapper.java216
-rw-r--r--src/com/android/tv/SetupPassthroughActivity.java176
-rw-r--r--src/com/android/tv/TimeShiftManager.java26
-rw-r--r--src/com/android/tv/TvApplication.java162
-rw-r--r--src/com/android/tv/TvOptionsManager.java133
-rw-r--r--src/com/android/tv/config/DefaultConfigManager.java6
-rw-r--r--src/com/android/tv/config/RemoteConfig.java3
-rw-r--r--src/com/android/tv/config/RemoteConfigUtils.java42
-rw-r--r--src/com/android/tv/customization/TvCustomizationManager.java118
-rw-r--r--src/com/android/tv/data/BaseProgram.java39
-rw-r--r--src/com/android/tv/data/Channel.java114
-rw-r--r--src/com/android/tv/data/ChannelDataManager.java200
-rw-r--r--src/com/android/tv/data/ChannelLogoFetcher.java307
-rw-r--r--src/com/android/tv/data/ChannelNumber.java71
-rw-r--r--src/com/android/tv/data/InternalDataUtils.java2
-rw-r--r--src/com/android/tv/data/PreviewDataManager.java636
-rw-r--r--src/com/android/tv/data/PreviewProgramContent.java259
-rw-r--r--src/com/android/tv/data/Program.java140
-rw-r--r--src/com/android/tv/data/ProgramDataManager.java76
-rw-r--r--src/com/android/tv/data/StreamInfo.java4
-rw-r--r--src/com/android/tv/data/WatchedHistoryManager.java161
-rw-r--r--src/com/android/tv/data/epg/EpgFetchHelper.java233
-rw-r--r--src/com/android/tv/data/epg/EpgFetcher.java988
-rw-r--r--src/com/android/tv/data/epg/EpgReader.java45
-rw-r--r--src/com/android/tv/data/epg/StubEpgReader.java37
-rw-r--r--src/com/android/tv/dialog/DvrHistoryDialogFragment.java130
-rw-r--r--src/com/android/tv/dialog/FullscreenDialogFragment.java2
-rw-r--r--src/com/android/tv/dialog/HalfSizedDialogFragment.java (renamed from src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java)24
-rw-r--r--src/com/android/tv/dialog/PinDialogFragment.java103
-rw-r--r--src/com/android/tv/dialog/SafeDismissDialogFragment.java26
-rw-r--r--src/com/android/tv/dialog/WebDialogFragment.java17
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java41
-rw-r--r--src/com/android/tv/dvr/DvrDataManager.java12
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java151
-rw-r--r--src/com/android/tv/dvr/DvrManager.java113
-rw-r--r--src/com/android/tv/dvr/DvrRecordingService.java122
-rw-r--r--src/com/android/tv/dvr/DvrScheduleManager.java139
-rw-r--r--src/com/android/tv/dvr/DvrStorageStatusManager.java35
-rw-r--r--src/com/android/tv/dvr/DvrWatchedPositionManager.java4
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java6
-rw-r--r--src/com/android/tv/dvr/data/IdGenerator.java (renamed from src/com/android/tv/dvr/IdGenerator.java)2
-rw-r--r--src/com/android/tv/dvr/data/RecordedProgram.java (renamed from src/com/android/tv/dvr/RecordedProgram.java)89
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java (renamed from src/com/android/tv/dvr/ScheduledRecording.java)27
-rw-r--r--src/com/android/tv/dvr/data/SeasonEpisodeNumber.java72
-rw-r--r--src/com/android/tv/dvr/data/SeriesInfo.java (renamed from src/com/android/tv/dvr/SeriesInfo.java)2
-rw-r--r--src/com/android/tv/dvr/data/SeriesRecording.java (renamed from src/com/android/tv/dvr/SeriesRecording.java)4
-rw-r--r--src/com/android/tv/dvr/provider/AsyncDvrDbTask.java4
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java4
-rw-r--r--src/com/android/tv/dvr/provider/DvrDbSync.java (renamed from src/com/android/tv/dvr/DvrDbSync.java)44
-rw-r--r--src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java (renamed from src/com/android/tv/dvr/EpisodicProgramLoadTask.java)73
-rw-r--r--src/com/android/tv/dvr/recorder/ConflictChecker.java (renamed from src/com/android/tv/dvr/ConflictChecker.java)5
-rw-r--r--src/com/android/tv/dvr/recorder/DvrRecordingService.java207
-rw-r--r--src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java (renamed from src/com/android/tv/dvr/DvrStartRecordingReceiver.java)14
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java (renamed from src/com/android/tv/dvr/InputTaskScheduler.java)20
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingScheduler.java (renamed from src/com/android/tv/dvr/Scheduler.java)210
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java (renamed from src/com/android/tv/dvr/RecordingTask.java)24
-rw-r--r--src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java (renamed from src/com/android/tv/dvr/ScheduledProgramReaper.java)5
-rw-r--r--src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java (renamed from src/com/android/tv/dvr/SeriesRecordingScheduler.java)84
-rw-r--r--src/com/android/tv/dvr/ui/BigArguments.java54
-rw-r--r--src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java79
-rw-r--r--src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java59
-rw-r--r--src/com/android/tv/dvr/ui/DetailsContent.java207
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java26
-rw-r--r--src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java26
-rw-r--r--src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java25
-rw-r--r--src/com/android/tv/dvr/ui/DvrConflictFragment.java48
-rw-r--r--src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java116
-rw-r--r--src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java39
-rw-r--r--src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java101
-rw-r--r--src/com/android/tv/dvr/ui/DvrItemPresenter.java80
-rw-r--r--src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java67
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java82
-rw-r--r--src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java304
-rw-r--r--src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java (renamed from src/com/android/tv/dvr/ui/PrioritySettingsFragment.java)29
-rw-r--r--src/com/android/tv/dvr/ui/DvrScheduleFragment.java36
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java5
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesDeletionFragment.java)11
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java62
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java20
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesSettingsFragment.java)209
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java29
-rw-r--r--src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java20
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java (renamed from src/com/android/tv/dvr/DvrUiHelper.java)383
-rw-r--r--src/com/android/tv/dvr/ui/FadeBackground.java70
-rw-r--r--src/com/android/tv/dvr/ui/RecordedProgramPresenter.java182
-rw-r--r--src/com/android/tv/dvr/ui/RecordingDetailsFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java177
-rw-r--r--src/com/android/tv/dvr/ui/SortedArrayAdapter.java90
-rw-r--r--src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java82
-rw-r--r--src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java (renamed from src/com/android/tv/dvr/ui/ActionPresenterSelector.java)10
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java120
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java317
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java (renamed from src/com/android/tv/dvr/ui/DetailsContentPresenter.java)65
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java (renamed from src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java)4
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java (renamed from src/com/android/tv/dvr/ui/DvrActivity.java)23
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java (renamed from src/com/android/tv/dvr/ui/DvrBrowseFragment.java)192
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java (renamed from src/com/android/tv/dvr/ui/DvrDetailsActivity.java)60
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/DvrDetailsFragment.java)83
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java140
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java34
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java (renamed from src/com/android/tv/dvr/ui/FullScheduleCardHolder.java)2
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java (renamed from src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java)68
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java)33
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java142
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java (renamed from src/com/android/tv/dvr/ui/RecordingCardView.java)141
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java51
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java)6
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java138
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java (renamed from src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java)41
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java (renamed from src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java)62
-rw-r--r--src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java (renamed from src/com/android/tv/dvr/ui/DvrSchedulesActivity.java)76
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java5
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java72
-rw-r--r--src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java9
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java8
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java4
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java50
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java20
-rw-r--r--src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java26
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java24
-rw-r--r--src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java17
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java (renamed from src/com/android/tv/dvr/DvrPlaybackActivity.java)35
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java45
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java (renamed from src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java)164
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java (renamed from src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java)104
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java494
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java154
-rw-r--r--src/com/android/tv/dvr/ui/playback/DvrPlayer.java (renamed from src/com/android/tv/dvr/DvrPlayer.java)222
-rw-r--r--src/com/android/tv/experiments/ExperimentFlag.java32
-rw-r--r--src/com/android/tv/experiments/Experiments.java9
-rw-r--r--src/com/android/tv/guide/GenreListAdapter.java23
-rw-r--r--src/com/android/tv/guide/GuideUtils.java110
-rw-r--r--src/com/android/tv/guide/ProgramGrid.java270
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java265
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java33
-rw-r--r--src/com/android/tv/guide/ProgramListAdapter.java36
-rw-r--r--src/com/android/tv/guide/ProgramManager.java656
-rw-r--r--src/com/android/tv/guide/ProgramRow.java33
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java172
-rw-r--r--src/com/android/tv/guide/TimeListAdapter.java35
-rw-r--r--src/com/android/tv/license/License.java112
-rw-r--r--src/com/android/tv/license/LicenseDialogFragment.java97
-rw-r--r--src/com/android/tv/license/LicenseSideFragment.java80
-rw-r--r--src/com/android/tv/license/LicenseUtils.java12
-rw-r--r--src/com/android/tv/license/Licenses.java122
-rw-r--r--src/com/android/tv/menu/ActionCardView.java6
-rw-r--r--src/com/android/tv/menu/AppLinkCardView.java286
-rw-r--r--src/com/android/tv/menu/BaseCardView.java86
-rw-r--r--src/com/android/tv/menu/ChannelCardView.java140
-rw-r--r--src/com/android/tv/menu/ChannelsPosterPrefetcher.java9
-rw-r--r--src/com/android/tv/menu/ChannelsRow.java10
-rw-r--r--src/com/android/tv/menu/ChannelsRowAdapter.java198
-rw-r--r--src/com/android/tv/menu/ChannelsRowItem.java101
-rw-r--r--src/com/android/tv/menu/ItemListRowView.java20
-rw-r--r--src/com/android/tv/menu/Menu.java43
-rw-r--r--src/com/android/tv/menu/MenuAction.java69
-rw-r--r--src/com/android/tv/menu/MenuLayoutManager.java54
-rw-r--r--src/com/android/tv/menu/MenuRow.java5
-rw-r--r--src/com/android/tv/menu/MenuRowFactory.java33
-rw-r--r--src/com/android/tv/menu/MenuUpdater.java48
-rw-r--r--src/com/android/tv/menu/OptionsRowAdapter.java50
-rw-r--r--src/com/android/tv/menu/PartnerOptionsRowAdapter.java3
-rw-r--r--src/com/android/tv/menu/PipOptionsRowAdapter.java137
-rw-r--r--src/com/android/tv/menu/PlayControlsButton.java24
-rw-r--r--src/com/android/tv/menu/PlayControlsRowView.java194
-rw-r--r--src/com/android/tv/menu/PlaybackProgressBar.java168
-rw-r--r--src/com/android/tv/menu/SimpleCardView.java4
-rw-r--r--src/com/android/tv/menu/TvOptionsRowAdapter.java138
-rw-r--r--src/com/android/tv/onboarding/SetupSourcesFragment.java2
-rw-r--r--src/com/android/tv/parental/ContentRatingSystem.java9
-rw-r--r--src/com/android/tv/parental/ContentRatingsManager.java19
-rw-r--r--src/com/android/tv/parental/ParentalControlSettings.java33
-rw-r--r--src/com/android/tv/perf/EventNames.java56
-rw-r--r--src/com/android/tv/perf/PerformanceMonitor.java99
-rw-r--r--src/com/android/tv/perf/StubPerformanceMonitor.java65
-rw-r--r--src/com/android/tv/perf/TimerEvent.java20
-rw-r--r--src/com/android/tv/receiver/BootCompletedReceiver.java27
-rw-r--r--src/com/android/tv/receiver/GlobalKeyReceiver.java52
-rw-r--r--src/com/android/tv/receiver/PackageIntentsReceiver.java12
-rw-r--r--src/com/android/tv/recommendation/ChannelPreviewUpdater.java323
-rw-r--r--src/com/android/tv/recommendation/NotificationService.java11
-rw-r--r--src/com/android/tv/recommendation/RecommendationDataManager.java6
-rw-r--r--src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java176
-rw-r--r--src/com/android/tv/search/DataManagerSearch.java4
-rw-r--r--src/com/android/tv/search/LocalSearchProvider.java105
-rw-r--r--src/com/android/tv/search/SearchInterface.java4
-rw-r--r--src/com/android/tv/search/TvProviderSearch.java10
-rw-r--r--src/com/android/tv/tuner/ChannelScanFileParser.java4
-rw-r--r--src/com/android/tv/tuner/DvbTunerHal.java (renamed from src/com/android/tv/tuner/UsbTunerHal.java)13
-rw-r--r--src/com/android/tv/tuner/TunerHal.java129
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java338
-rw-r--r--src/com/android/tv/tuner/TunerPreferences.java196
-rw-r--r--src/com/android/tv/tuner/cc/CaptionTrackRenderer.java4
-rw-r--r--src/com/android/tv/tuner/cc/Cea708Parser.java14
-rw-r--r--src/com/android/tv/tuner/data/PsipData.java133
-rw-r--r--src/com/android/tv/tuner/data/TunerChannel.java163
-rw-r--r--src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java29
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java41
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java378
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java14
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java89
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java20
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioClock.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/AudioClock.java)2
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java70
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java)22
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java)22
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java235
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java)295
-rw-r--r--src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java)24
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java280
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java209
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java24
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java115
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java8
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java85
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java249
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java205
-rw-r--r--src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl29
-rw-r--r--src/com/android/tv/tuner/setup/ConnectionTypeFragment.java19
-rw-r--r--src/com/android/tv/tuner/setup/PostalCodeFragment.java178
-rw-r--r--src/com/android/tv/tuner/setup/ScanFragment.java72
-rw-r--r--src/com/android/tv/tuner/setup/ScanResultFragment.java17
-rw-r--r--src/com/android/tv/tuner/setup/TunerSetupActivity.java375
-rw-r--r--src/com/android/tv/tuner/setup/WelcomeFragment.java54
-rw-r--r--src/com/android/tv/tuner/source/FileTsStreamer.java24
-rw-r--r--src/com/android/tv/tuner/source/TsDataSourceManager.java16
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamer.java79
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamerManager.java25
-rw-r--r--src/com/android/tv/tuner/ts/SectionParser.java569
-rw-r--r--src/com/android/tv/tuner/ts/TsParser.java74
-rw-r--r--src/com/android/tv/tuner/tvinput/ChannelDataManager.java54
-rw-r--r--src/com/android/tv/tuner/tvinput/EventDetector.java101
-rw-r--r--src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java45
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerDebug.java4
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java116
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSession.java38
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSessionWorker.java457
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java8
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerTvInputService.java35
-rw-r--r--src/com/android/tv/tuner/util/PostalCodeUtils.java138
-rw-r--r--src/com/android/tv/tuner/util/SystemPropertiesProxy.java16
-rw-r--r--src/com/android/tv/tuner/util/TunerInputInfoUtils.java87
-rw-r--r--src/com/android/tv/ui/AppLayerTvView.java14
-rw-r--r--src/com/android/tv/ui/BlockScreenView.java137
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java189
-rw-r--r--src/com/android/tv/ui/KeypadChannelSwitchView.java2
-rw-r--r--src/com/android/tv/ui/SelectInputView.java74
-rw-r--r--src/com/android/tv/ui/TunableTvView.java644
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java231
-rw-r--r--src/com/android/tv/ui/TvTransitionManager.java2
-rw-r--r--src/com/android/tv/ui/TvViewUiManager.java408
-rw-r--r--src/com/android/tv/ui/sidepanel/ActionItem.java20
-rw-r--r--src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java108
-rw-r--r--src/com/android/tv/ui/sidepanel/CompoundButtonItem.java21
-rw-r--r--src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java116
-rw-r--r--src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java45
-rw-r--r--src/com/android/tv/ui/sidepanel/Item.java12
-rw-r--r--src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java170
-rw-r--r--src/com/android/tv/ui/sidepanel/SettingsFragment.java137
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragment.java158
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragmentManager.java38
-rw-r--r--src/com/android/tv/ui/sidepanel/SimpleActionItem.java (renamed from src/com/android/tv/ui/sidepanel/SimpleItem.java)6
-rw-r--r--src/com/android/tv/ui/sidepanel/SubMenuItem.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/SwitchItem.java5
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java13
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java42
-rw-r--r--src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java32
-rw-r--r--src/com/android/tv/util/AsyncDbTask.java4
-rw-r--r--src/com/android/tv/util/BitmapUtils.java10
-rw-r--r--src/com/android/tv/util/Debug.java60
-rw-r--r--src/com/android/tv/util/DurationTimer.java (renamed from src/com/android/tv/analytics/DurationTimer.java)41
-rw-r--r--src/com/android/tv/util/ImageLoader.java3
-rw-r--r--src/com/android/tv/util/LocationUtils.java25
-rw-r--r--src/com/android/tv/util/NetworkTrafficTags.java64
-rw-r--r--src/com/android/tv/util/OnboardingUtils.java41
-rw-r--r--src/com/android/tv/util/Partner.java181
-rw-r--r--src/com/android/tv/util/PermissionUtils.java5
-rw-r--r--src/com/android/tv/util/PipInputManager.java432
-rw-r--r--src/com/android/tv/util/RecurringRunner.java9
-rw-r--r--src/com/android/tv/util/SearchManagerHelper.java61
-rw-r--r--src/com/android/tv/util/SetupUtils.java25
-rw-r--r--src/com/android/tv/util/StringUtils.java (renamed from src/com/android/tv/tuner/util/StringUtils.java)2
-rw-r--r--src/com/android/tv/util/TimeShiftUtils.java4
-rw-r--r--src/com/android/tv/util/TvInputManagerHelper.java358
-rw-r--r--src/com/android/tv/util/TvSettings.java160
-rw-r--r--src/com/android/tv/util/TvTrackInfoUtils.java37
-rw-r--r--src/com/android/tv/util/TvUriMatcher.java (renamed from src/com/android/tv/util/TvProviderUriMatcher.java)14
-rw-r--r--src/com/android/tv/util/Utils.java100
-rw-r--r--src/com/android/tv/util/ViewCache.java100
302 files changed, 19659 insertions, 9932 deletions
diff --git a/src/com/android/exoplayer/text/SubtitleView.java b/src/com/android/exoplayer/text/SubtitleView.java
index b1161f22..37926eda 100644
--- a/src/com/android/exoplayer/text/SubtitleView.java
+++ b/src/com/android/exoplayer/text/SubtitleView.java
@@ -39,14 +39,12 @@ import java.util.ArrayList;
/**
* Since this class does not exist in recent version of ExoPlayer and used by
- * {@link com.google.android.tv.tuner.cc.CaptionWindowLayout}, this class is copied from
+ * {@link com.android.tv.tuner.cc.CaptionWindowLayout}, this class is copied from
* older version of ExoPlayer.
* A view for rendering a single caption.
*/
@Deprecated
public class SubtitleView extends View {
- // TODO: Change usage of this class to up-to-date class of ExoPlayer.
-
/**
* Ratio of inner padding to font size.
*/
diff --git a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
new file mode 100644
index 00000000..2b7817dc
--- /dev/null
+++ b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegAudioDecoder.java
@@ -0,0 +1,127 @@
+/*
+ * 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.google.android.exoplayer2.ext.ffmpeg;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.android.tv.common.SoftPreconditions;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Audio decoder which uses ffmpeg extension of ExoPlayer2. Since {@link FfmpegDecoder} is package
+ * private, expose the decoder via this class. Supported formats are AC3 and MP2.
+ */
+public class FfmpegAudioDecoder {
+ private static final int NUM_DECODER_BUFFERS = 1;
+
+ // The largest AC3 sample size. This is bigger than the largest MP2 sample size (1729).
+ private static final int INITIAL_INPUT_BUFFER_SIZE = 2560;
+ private static boolean AVAILABLE;
+
+ static {
+ AVAILABLE =
+ FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_AC3)
+ && FfmpegLibrary.supportsFormat(MimeTypes.AUDIO_MPEG_L2);
+ }
+
+ private FfmpegDecoder mDecoder;
+ private DecoderInputBuffer mInputBuffer;
+ private SimpleOutputBuffer mOutputBuffer;
+ private boolean mStarted;
+
+ /** Return whether Ffmpeg based software audio decoder is available. */
+ public static boolean isAvailable() {
+ return AVAILABLE;
+ }
+
+ /** Creates an Ffmpeg based software audio decoder. */
+ public FfmpegAudioDecoder(Context context) {
+ if (context.checkSelfPermission("android.permission.INTERNET")
+ == PackageManager.PERMISSION_GRANTED) {
+ throw new IllegalStateException("This code should run in an isolated process");
+ }
+ }
+
+ /**
+ * Decodes an audio sample.
+ *
+ * @param timeUs presentation timestamp of the sample
+ * @param sample data
+ */
+ public void decode(long timeUs, byte[] sample) {
+ SoftPreconditions.checkState(AVAILABLE);
+ mInputBuffer.data.clear();
+ mInputBuffer.data.put(sample);
+ mInputBuffer.data.flip();
+ mInputBuffer.timeUs = timeUs;
+ mDecoder.decode(mInputBuffer, mOutputBuffer, !mStarted);
+ if (!mStarted) {
+ mStarted = true;
+ }
+ }
+
+ /** Returns a decoded sample from decoder. */
+ public ByteBuffer getDecodedSample() {
+ return mOutputBuffer.data;
+ }
+
+ /** Returns the presentation time for the decoded sample. */
+ public long getDecodedTimeUs() {
+ return mOutputBuffer.timeUs;
+ }
+
+ /**
+ * Clear previous decode state if any. Prepares to decode samples of the specified encoding.
+ * This method should be called before using decode.
+ *
+ * @param mime audio encoding
+ */
+ public void resetDecoderState(String mime) {
+ SoftPreconditions.checkState(AVAILABLE);
+ release();
+ try {
+ mDecoder =
+ new FfmpegDecoder(
+ NUM_DECODER_BUFFERS,
+ NUM_DECODER_BUFFERS,
+ INITIAL_INPUT_BUFFER_SIZE,
+ mime,
+ null);
+ mStarted = false;
+ mInputBuffer = mDecoder.createInputBuffer();
+ // Since native JNI requires direct buffer, we should allocate it by #allocateDirect.
+ mInputBuffer.data = ByteBuffer.allocateDirect(INITIAL_INPUT_BUFFER_SIZE);
+ mOutputBuffer = mDecoder.createOutputBuffer();
+ } catch (FfmpegDecoderException e) {
+ // if AVAILABLE is {@code true}, this will not happen.
+ }
+ }
+
+ /** Releases all the resource. */
+ public void release() {
+ SoftPreconditions.checkState(AVAILABLE);
+ if (mDecoder != null) {
+ mDecoder.release();
+ mInputBuffer = null;
+ mOutputBuffer = null;
+ mDecoder = null;
+ }
+ }
+}
diff --git a/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
new file mode 100644
index 00000000..daa77340
--- /dev/null
+++ b/src/com/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java
@@ -0,0 +1,84 @@
+/*
+ * 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.google.android.exoplayer2.ext.ffmpeg;
+
+import com.google.android.exoplayer2.util.LibraryLoader;
+import com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * This class is based on com.google.android.exoplayer2.ext.ffmpeg.FfmpegLibrary from ExoPlayer2
+ * in order to support mp2 decoder.
+ * Configures and queries the underlying native library.
+ */
+public final class FfmpegLibrary {
+
+ private static final LibraryLoader LOADER =
+ new LibraryLoader("avutil", "avresample", "avcodec", "ffmpeg");
+
+ private FfmpegLibrary() {}
+
+ /**
+ * Overrides the names of the FFmpeg native libraries. If an application wishes to call this
+ * method, it must do so before calling any other method defined by this class, and before
+ * instantiating a {@link FfmpegAudioRenderer} instance.
+ */
+ public static void setLibraries(String... libraries) {
+ LOADER.setLibraries(libraries);
+ }
+
+ /**
+ * Returns whether the underlying library is available, loading it if necessary.
+ */
+ public static boolean isAvailable() {
+ return LOADER.isAvailable();
+ }
+
+ /**
+ * Returns the version of the underlying library if available, or null otherwise.
+ */
+ public static String getVersion() {
+ return isAvailable() ? ffmpegGetVersion() : null;
+ }
+
+ /**
+ * Returns whether the underlying library supports the specified MIME type.
+ */
+ public static boolean supportsFormat(String mimeType) {
+ if (!isAvailable()) {
+ return false;
+ }
+ String codecName = getCodecName(mimeType);
+ return codecName != null && ffmpegHasDecoder(codecName);
+ }
+
+ /**
+ * Returns the name of the FFmpeg decoder that could be used to decode {@code mimeType}.
+ */
+ /* package */ static String getCodecName(String mimeType) {
+ switch (mimeType) {
+ case MimeTypes.AUDIO_MPEG_L2:
+ return "mp2";
+ case MimeTypes.AUDIO_AC3:
+ return "ac3";
+ default:
+ return null;
+ }
+ }
+
+ private static native String ffmpegGetVersion();
+ private static native boolean ffmpegHasDecoder(String codecName);
+
+}
diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/ApplicationSingletons.java
index fd125d52..ac7d4c4a 100644
--- a/src/com/android/tv/ApplicationSingletons.java
+++ b/src/com/android/tv/ApplicationSingletons.java
@@ -20,12 +20,15 @@ import com.android.tv.analytics.Analytics;
import com.android.tv.analytics.Tracker;
import com.android.tv.config.RemoteConfig;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.PreviewDataManager;
import com.android.tv.data.ProgramDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.DvrStorageStatusManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.recorder.RecordingScheduler;
+import com.android.tv.perf.PerformanceMonitor;
import com.android.tv.util.AccountHelper;
import com.android.tv.util.TvInputManagerHelper;
@@ -38,6 +41,22 @@ public interface ApplicationSingletons {
ChannelDataManager getChannelDataManager();
+ /**
+ * Checks if the {@link ChannelDataManager} instance has been created and all the channels has
+ * been loaded.
+ */
+ boolean isChannelDataManagerLoadFinished();
+
+ ProgramDataManager getProgramDataManager();
+
+ /**
+ * Checks if the {@link ProgramDataManager} instance has been created and the current programs
+ * for all the channels has been loaded.
+ */
+ boolean isProgramDataManagerCurrentProgramsLoadFinished();
+
+ PreviewDataManager getPreviewDataManager();
+
DvrDataManager getDvrDataManager();
DvrStorageStatusManager getDvrStorageStatusManager();
@@ -46,12 +65,12 @@ public interface ApplicationSingletons {
DvrManager getDvrManager();
+ RecordingScheduler getRecordingScheduler();
+
DvrWatchedPositionManager getDvrWatchedPositionManager();
InputSessionManager getInputSessionManager();
- ProgramDataManager getProgramDataManager();
-
Tracker getTracker();
TvInputManagerHelper getTvInputManagerHelper();
@@ -61,4 +80,8 @@ public interface ApplicationSingletons {
AccountHelper getAccountHelper();
RemoteConfig getRemoteConfig();
+
+ boolean isRunningInMainProcess();
+
+ PerformanceMonitor getPerformanceMonitor();
}
diff --git a/src/com/android/tv/AudioManagerHelper.java b/src/com/android/tv/AudioManagerHelper.java
new file mode 100644
index 00000000..4fca06ac
--- /dev/null
+++ b/src/com/android/tv/AudioManagerHelper.java
@@ -0,0 +1,108 @@
+package com.android.tv;
+
+import android.app.Activity;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+
+import com.android.tv.receiver.AudioCapabilitiesReceiver;
+import com.android.tv.ui.TunableTvView;
+
+/**
+ * A helper class to help {@link MainActivity} to handle audio-related stuffs.
+ */
+class AudioManagerHelper implements AudioManager.OnAudioFocusChangeListener {
+ private static final float AUDIO_MAX_VOLUME = 1.0f;
+ private static final float AUDIO_MIN_VOLUME = 0.0f;
+ private static final float AUDIO_DUCKING_VOLUME = 0.3f;
+
+ private final Activity mActivity;
+ private final TunableTvView mTvView;
+ private final AudioManager mAudioManager;
+ private final AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
+
+ private boolean mAc3PassthroughSupported;
+ private int mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
+
+ AudioManagerHelper(Activity activity, TunableTvView tvView) {
+ mActivity = activity;
+ mTvView = tvView;
+ mAudioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
+ mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(activity,
+ new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
+ @Override
+ public void onAc3PassthroughCapabilityChange(boolean capability) {
+ mAc3PassthroughSupported = capability;
+ }
+ });
+ mAudioCapabilitiesReceiver.register();
+ }
+
+ /**
+ * Sets suitable volume to {@link TunableTvView} according to the current audio focus.
+ * If the focus status is {@link AudioManager#AUDIOFOCUS_LOSS} and the activity is under PIP
+ * mode, this method will finish the activity.
+ */
+ void setVolumeByAudioFocusStatus() {
+ if (mTvView.isPlaying()) {
+ switch (mAudioFocusStatus) {
+ case AudioManager.AUDIOFOCUS_GAIN:
+ mTvView.setStreamVolume(AUDIO_MAX_VOLUME);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS:
+ if (Features.PICTURE_IN_PICTURE.isEnabled(mActivity)
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && mActivity.isInPictureInPictureMode()) {
+ mActivity.finish();
+ break;
+ }
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ mTvView.setStreamVolume(AUDIO_MIN_VOLUME);
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ mTvView.setStreamVolume(AUDIO_DUCKING_VOLUME);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Tries to request audio focus from {@link AudioManager} and set volume according to the
+ * returned result.
+ */
+ void requestAudioFocus() {
+ int result = mAudioManager.requestAudioFocus(this,
+ AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+ mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
+ AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
+ setVolumeByAudioFocusStatus();
+ }
+
+ /**
+ * Abandons audio focus.
+ */
+ void abandonAudioFocus() {
+ mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
+ mAudioManager.abandonAudioFocus(this);
+ }
+
+ /**
+ * Returns {@code true} if the device supports AC3 pass-through.
+ */
+ boolean isAc3PassthroughSupported() {
+ return mAc3PassthroughSupported;
+ }
+
+ /**
+ * Release the resources the helper class may occupied.
+ */
+ void release() {
+ mAudioCapabilitiesReceiver.unregister();
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ mAudioFocusStatus = focusChange;
+ setVolumeByAudioFocusStatus();
+ }
+}
diff --git a/src/com/android/tv/Features.java b/src/com/android/tv/Features.java
index 7e8e3689..2052f2e7 100644
--- a/src/com/android/tv/Features.java
+++ b/src/com/android/tv/Features.java
@@ -16,22 +16,29 @@
package com.android.tv;
-import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
-import static com.android.tv.common.feature.FeatureUtils.AND;
-import static com.android.tv.common.feature.FeatureUtils.OFF;
-import static com.android.tv.common.feature.FeatureUtils.ON;
-import static com.android.tv.common.feature.FeatureUtils.OR;
-
import android.content.Context;
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.experiments.Experiments;
+import com.android.tv.util.LocationUtils;
import com.android.tv.util.PermissionUtils;
+import com.android.tv.util.Utils;
+
+import java.util.Locale;
+
+import static com.android.tv.common.feature.EngOnlyFeature.ENG_ONLY_FEATURE;
+import static com.android.tv.common.feature.FeatureUtils.AND;
+import static com.android.tv.common.feature.FeatureUtils.OFF;
+import static com.android.tv.common.feature.FeatureUtils.ON;
+import static com.android.tv.common.feature.FeatureUtils.OR;
/**
* List of {@link Feature} for the Live TV App.
@@ -39,6 +46,9 @@ import com.android.tv.util.PermissionUtils;
* <p>Remove the {@code Feature} once it is launched.
*/
public final class Features {
+ private static final String TAG = "Features";
+ private static final boolean DEBUG = false;
+
/**
* UI for opting in to analytics.
*
@@ -57,17 +67,40 @@ public final class Features {
public static final Feature EPG_SEARCH =
new PropertyFeature("feature_tv_use_epg_search", false);
- public static final Feature TUNER = new Feature() {
- @Override
- public boolean isEnabled(Context context) {
+ public static final Feature TUNER =
+ new Feature() {
+ @Override
+ public boolean isEnabled(Context context) {
- // 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()}.
- return Build.VERSION.SDK_INT > Build.VERSION_CODES.M || BuildCompat.isAtLeastN();
- }
+ 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
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
+ }
+ };
+
+ /**
+ * 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";
/**
@@ -82,22 +115,75 @@ public final class Features {
}
});
- public static final Feature PICTURE_IN_PICTURE = new Feature() {
- private Boolean mEnabled;
+ public static final Feature PICTURE_IN_PICTURE =
+ new Feature() {
+ private Boolean mEnabled;
- @Override
- public boolean isEnabled(Context context) {
- if (mEnabled == null) {
- mEnabled = context.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_PICTURE_IN_PICTURE);
- }
- return mEnabled;
- }
- };
+ @Override
+ public boolean isEnabled(Context context) {
+ if (mEnabled == null) {
+ mEnabled =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && context.getPackageManager()
+ .hasSystemFeature(
+ PackageManager.FEATURE_PICTURE_IN_PICTURE);
+ }
+ return mEnabled;
+ }
+ };
- /**
- * Enable a conflict dialog between currently watched channel and upcoming recording.
- */
+ /** Use AC3 software decode. */
+ public static final Feature AC3_SOFTWARE_DECODE =
+ new Feature() {
+ private final String[] SUPPORTED_REGIONS = {};
+
+ 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_REGIONS.length; ++i) {
+ if (SUPPORTED_REGIONS[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;
+ }
+ };
+
+ /** Show postal code fragment before channel scan. */
+ public static final Feature ENABLE_CLOUD_EPG_REGION =
+ new Feature() {
+ private final String[] SUPPORTED_REGIONS = {
+ };
+
+
+ @Override
+ public boolean isEnabled(Context context) {
+ if (!Experiments.CLOUD_EPG.get()) {
+ if (DEBUG) Log.d(TAG, "Experiments.CLOUD_EPG is false");
+ return false;
+ }
+ String country = LocationUtils.getCurrentCountry(context);
+ for (int i = 0; i < SUPPORTED_REGIONS.length; i++) {
+ if (SUPPORTED_REGIONS[i].equalsIgnoreCase(country)) {
+ return true;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "EPG flag false after country check");
+ return false;
+ }
+ };
+
+ /** Enable a conflict dialog between currently watched channel and upcoming recording. */
public static final Feature SHOW_UPCOMING_CONFLICT_DIALOG = OFF;
/**
@@ -105,6 +191,11 @@ public final class Features {
*/
public static final Feature USE_PARTNER_INPUT_BLACKLIST = ON;
+ /**
+ * Enable Dvb parsers and listeners.
+ */
+ public static final Feature ENABLE_FILE_DVB = OFF;
+
@VisibleForTesting
public static final Feature TEST_FEATURE = new PropertyFeature("test_feature", false);
diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java
index e4b0f456..2978f409 100644
--- a/src/com/android/tv/InputSessionManager.java
+++ b/src/com/android/tv/InputSessionManager.java
@@ -37,7 +37,6 @@ import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
-import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnTuneListener;
@@ -73,6 +72,8 @@ public class InputSessionManager {
Collections.synchronizedSet(new ArraySet<>());
private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners =
new ArraySet<>();
+ private final Set<OnRecordingSessionChangeListener> mOnRecordingSessionChangeListeners =
+ new ArraySet<>();
public InputSessionManager(Context context) {
mContext = context.getApplicationContext();
@@ -113,6 +114,9 @@ public class InputSessionManager {
RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
mRecordingSessions.add(session);
if (DEBUG) Log.d(TAG, "Recording session created: " + session);
+ for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) {
+ listener.onRecordingSessionChange(true, mRecordingSessions.size());
+ }
return session;
}
@@ -123,6 +127,9 @@ public class InputSessionManager {
mRecordingSessions.remove(session);
session.release();
if (DEBUG) Log.d(TAG, "Recording session released: " + session);
+ for (OnRecordingSessionChangeListener listener : mOnRecordingSessionChangeListeners) {
+ listener.onRecordingSessionChange(false, mRecordingSessions.size());
+ }
}
/**
@@ -148,9 +155,17 @@ public class InputSessionManager {
}
}
- /**
- * Returns the current {@link TvView} channel.
- */
+ /** Adds the {@link OnRecordingSessionChangeListener}. */
+ public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
+ mOnRecordingSessionChangeListeners.add(listener);
+ }
+
+ /** Removes the {@link OnRecordingSessionChangeListener}. */
+ public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
+ mOnRecordingSessionChangeListeners.remove(listener);
+ }
+
+ /** Returns the current {@link TvView} channel. */
@MainThread
public Uri getCurrentTvViewChannelUri() {
for (TvViewSession session : mTvViewSessions) {
@@ -249,7 +264,7 @@ public class InputSessionManager {
mTvView.setCallback(new DelegateTvInputCallback(mCallback) {
@Override
public void onConnectionFailed(String inputId) {
- if (DEBUG) Log.d(TAG, "TvViewSession: commection failed");
+ if (DEBUG) Log.d(TAG, "TvViewSession: connection failed");
mTuned = false;
mNeedToBeRetuned = false;
super.onConnectionFailed(inputId);
@@ -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..ed5f79a1 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -27,14 +27,7 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
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;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Channels;
@@ -44,7 +37,6 @@ import android.media.tv.TvInputManager.TvInputCallback;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView.OnUnhandledInputEventListener;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -71,7 +63,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,26 +82,30 @@ 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.PinDialogFragment.OnPinCheckedListener;
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.experiments.Experiments;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.menu.Menu;
import com.android.tv.onboarding.OnboardingActivity;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
-import com.android.tv.receiver.AudioCapabilitiesReceiver;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.StubPerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
+import com.android.tv.recommendation.ChannelPreviewUpdater;
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.ui.AppLayerTvView;
import com.android.tv.ui.ChannelBannerView;
import com.android.tv.ui.InputBannerView;
import com.android.tv.ui.KeypadChannelSwitchView;
@@ -128,23 +123,22 @@ import com.android.tv.ui.sidepanel.DisplayModeFragment;
import com.android.tv.ui.sidepanel.MultiAudioFragment;
import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragment;
+import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
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;
@@ -159,8 +153,7 @@ import java.util.concurrent.TimeUnit;
/**
* The main activity for the Live TV app.
*/
-public class MainActivity extends Activity implements AudioManager.OnAudioFocusChangeListener,
- OnActionClickListener {
+public class MainActivity extends Activity implements OnActionClickListener, OnPinCheckedListener {
private static final String TAG = "MainActivity";
private static final boolean DEBUG = false;
@@ -175,18 +168,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private static final boolean USE_BACK_KEY_LONG_PRESS = false;
- private static final float AUDIO_MAX_VOLUME = 1.0f;
- private static final float AUDIO_MIN_VOLUME = 0.0f;
- private static final float AUDIO_DUCKING_VOLUME = 0.3f;
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 final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1;
- private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 2;
private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
// Tracker screen names.
@@ -211,49 +197,26 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
+ private static final IntentFilter SYSTEM_INTENT_FILTER = new IntentFilter();
+ static {
+ SYSTEM_INTENT_FILTER.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
+ SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_OFF);
+ SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_SCREEN_ON);
+ SYSTEM_INTENT_FILTER.addAction(Intent.ACTION_TIME_CHANGED);
+ }
+
private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
- private static final int REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS = 2;
private static final String KEY_INIT_CHANNEL_ID = "com.android.tv.init_channel_id";
- private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
-
// Change channels with key long press.
private static final int CHANNEL_CHANGE_NORMAL_SPEED_DURATION_MS = 3000;
private static final int CHANNEL_CHANGE_DELAY_MS_IN_MAX_SPEED = 50;
private static final int CHANNEL_CHANGE_DELAY_MS_IN_NORMAL_SPEED = 200;
private static final int CHANNEL_CHANGE_INITIAL_DELAY_MILLIS = 500;
- private static final int FIRST_STREAM_INFO_UPDATE_DELAY_MILLIS = 500;
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})
- private @interface ChannelBannerUpdateReason {}
- /**
- * Updates channel banner because the channel banner is forced to show.
- */
- private static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1;
- /**
- * Updates channel banner because of tuning.
- */
- private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2;
- /**
- * Updates channel banner because of fast tuning.
- */
- private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3;
- /**
- * Updates channel banner because of info updating.
- */
- private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4;
- /**
- * Updates channel banner because the current watched channel is locked or unlocked.
- */
- private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000;
@@ -261,12 +224,14 @@ 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 static final long START_UP_TIMER_RESET_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(3);
+
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,12 +243,7 @@ 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.
- private ChannelBannerView mChannelBannerView;
- private KeypadChannelSwitchView mKeypadChannelSwitchView;
@Nullable
private Uri mInitChannelUri;
@Nullable
@@ -293,20 +253,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mShowSelectInputView;
private TvInputInfo mInputToSetUp;
private final List<MemoryManageable> mMemoryManageables = new ArrayList<>();
- private MediaSession mMediaSession;
- private int mNowPlayingCardWidth;
- private int mNowPlayingCardHeight;
+ private MediaSessionWrapper mMediaSessionWrapper;
private final MyOnTuneListener mOnTuneListener = new MyOnTuneListener();
private String mInputIdUnderSetup;
private boolean mIsSetupActivityCalledByPopup;
- private AudioManager mAudioManager;
- private int mAudioFocusStatus;
+ private AudioManagerHelper mAudioManagerHelper;
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;
@@ -316,10 +269,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private boolean mBackKeyPressed;
private boolean mNeedShowBackKeyGuide;
private boolean mVisibleBehind;
- private boolean mAc3PassthroughSupported;
private boolean mShowNewSourcesFragment = true;
private String mTunerInputId;
private boolean mOtherActivityLaunched;
+ private PerformanceMonitor mPerformanceMonitor;
private boolean mIsFilmModeSet;
private float mDefaultRefreshRate;
@@ -331,11 +284,8 @@ 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
- // shrunken.
private TvContentRating mLastAllowedRatingForCurrentChannel;
private TvContentRating mAllowedRatingBeforeShrunken;
@@ -346,42 +296,46 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private static final int MAX_RECENT_CHANNELS = 5;
private final ArrayDeque<Long> mRecentChannels = new ArrayDeque<>(MAX_RECENT_CHANNELS);
- private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private RecurringRunner mSendConfigInfoRecurringRunner;
private RecurringRunner mChannelStatusRecurringRunner;
- // A caller which started this activity. (e.g. TvSearch)
- private String mSource;
-
private final Handler mHandler = new MainActivityHandler(this);
private final Set<OnActionClickListener> mOnActionClickListeners = new ArraySet<>();
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
- if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_OFF");
- // We need to stop TvView, when the screen is turned off. If not and TIS uses
- // MediaPlayer, a device may not go to the sleep mode and audio can be heard,
- // because MediaPlayer keeps playing media by its wake lock.
- mScreenOffIntentReceived = true;
- markCurrentChannelDuringScreenOff();
- stopAll(true);
- } else if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
- if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON");
- 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.
- 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);
- applyParentalControlSettings();
+ switch (intent.getAction()) {
+ case Intent.ACTION_SCREEN_OFF:
+ if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_OFF");
+ // We need to stop TvView, when the screen is turned off. If not and TIS uses
+ // MediaPlayer, a device may not go to the sleep mode and audio can be heard,
+ // because MediaPlayer keeps playing media by its wake lock.
+ mScreenOffIntentReceived = true;
+ markCurrentChannelDuringScreenOff();
+ stopAll(true);
+ break;
+ case Intent.ACTION_SCREEN_ON:
+ if (DEBUG) Log.d(TAG, "Received ACTION_SCREEN_ON");
+ 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 explicitly.
+ resumeTvIfNeeded();
+ }
+ break;
+ case TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED:
+ if (DEBUG) Log.d(TAG, "Received parental control settings change");
+ applyParentalControlSettings();
+ checkChannelLockNeeded(mTvView, null);
+ break;
+ case Intent.ACTION_TIME_CHANGED:
+ // Re-tune the current channel to prevent incorrect behavior of trick-play.
+ // See: b/37393628
+ if (mChannelTuner.getCurrentChannel() != null) {
+ tune(true);
+ }
+ break;
}
}
};
@@ -397,8 +351,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
Channel channel = mTvView.getCurrentChannel();
if (channel != null && channel.getId() == channelId) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
- updateMediaSession();
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
+ mMediaSessionWrapper.update(mTvView.isBlocked(), channel, program);
}
}
};
@@ -407,36 +362,39 @@ 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());
+ mOverlayManager.onBrowsableChannelsUpdated();
}
@Override
public void onBrowsableChannelListChanged() {
- mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList());
+ mOverlayManager.onBrowsableChannelsUpdated();
}
@Override
public void onCurrentChannelUnavailable(Channel channel) {
- // TODO: handle the case that a channel is suddenly removed from DB.
+ if (mChannelTuner.moveToAdjacentBrowsableChannel(true)) {
+ tune(true);
+ } else {
+ stopTv("onCurrentChannelUnavailable()", false);
+ }
}
@Override
- public void onChannelChanged(Channel previousChannel, Channel currentChannel) {
- }
+ public void onChannelChanged(Channel previousChannel, Channel currentChannel) {}
};
- 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,54 +414,54 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
boolean parentalControlEnabled = mTvInputManagerHelper.getParentalControlSettings()
.isParentalControlsEnabled();
mTvView.onParentalControlChanged(parentalControlEnabled);
- mPipView.onParentalControlChanged(parentalControlEnabled);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ChannelPreviewUpdater.getInstance(this).updatePreviewDataForChannelsImmediately();
+ }
}
@Override
protected void onCreate(Bundle savedInstanceState) {
+ TimerEvent timer = StubPerformanceMonitor.startBootstrapTimer();
+ DurationTimer startUpDebugTimer = Debug.getTimer(Debug.TAG_START_UP_TIMER);
+ if (!startUpDebugTimer.isStarted()
+ || startUpDebugTimer.getDuration() > START_UP_TIMER_RESET_THRESHOLD_MS) {
+ // TvApplication can start by other reason before MainActivty is launched.
+ // In this case, we restart the timer.
+ startUpDebugTimer.start();
+ }
+ startUpDebugTimer.log("MainActivity.onCreate");
if (DEBUG) Log.d(TAG,"onCreate()");
TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(this);
+ if (!applicationSingletons.getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ finishAndRemoveTask();
+ return;
+ }
+ mPerformanceMonitor = applicationSingletons.getPerformanceMonitor();
- boolean isPassthroughInput = TvContract.isChannelUriForPassthroughInput(getIntent()
- .getData());
- boolean skipToShowOnboarding = Intent.ACTION_VIEW.equals(getIntent().getAction())
+ TvApplication tvApplication = (TvApplication) getApplication();
+ mChannelDataManager = tvApplication.getChannelDataManager();
+ // In API 23, TvContract.isChannelUriForPassthroughInput is hidden.
+ boolean isPassthroughInput =
+ TvContract.isChannelUriForPassthroughInput(getIntent().getData());
+ boolean tuneToPassthroughInput = Intent.ACTION_VIEW.equals(getIntent().getAction())
&& isPassthroughInput;
- if (OnboardingUtils.needToShowOnboarding(this) && !skipToShowOnboarding
+ boolean channelLoadedAndNoChannelAvailable = mChannelDataManager.isDbLoadFinished()
+ && mChannelDataManager.getChannelCount() <= 0;
+ if ((OnboardingUtils.isFirstRunWithCurrentVersion(this)
+ || channelLoadedAndNoChannelAvailable)
+ && !tuneToPassthroughInput
&& !TvCommonUtils.isRunningInTest()) {
- // TODO: The onboarding is turned off in test, because tests are broken by the
- // onboarding. We need to enable the feature for tests later.
- startActivity(OnboardingActivity.buildIntent(this, getIntent()));
- finish();
+ startOnboardingActivity();
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);
+ 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(mProgramDataManager, mTvInputManagerHelper);
mTvView.setOnUnhandledInputEventListener(new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
@@ -526,7 +484,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 +491,21 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTvView.warmUpInput(inputId, TvContract.buildChannelUri(channelId));
}
- TvApplication tvApplication = (TvApplication) getApplication();
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());
@@ -565,28 +516,29 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
new OnCurrentProgramUpdatedListener() {
@Override
public void onCurrentProgramUpdated(long channelId, Program program) {
- updateMediaSession();
+ mMediaSessionWrapper.update(mTvView.isBlocked(), getCurrentChannel(),
+ program);
switch (mTimeShiftManager.getLastActionId()) {
case TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT:
- updateChannelBannerAndShowIfNeeded(
- UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
break;
case TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE:
case TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY:
default:
- updateChannelBannerAndShowIfNeeded(
- UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO);
break;
}
}
});
- 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,17 +546,15 @@ 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(
+ ChannelBannerView channelBannerView = (ChannelBannerView) getLayoutInflater().inflate(
R.layout.channel_banner, sceneContainer, false);
- mKeypadChannelSwitchView = (KeypadChannelSwitchView) getLayoutInflater().inflate(
- R.layout.keypad_channel_switch, sceneContainer, false);
+ KeypadChannelSwitchView keypadChannelSwitchView = (KeypadChannelSwitchView)
+ getLayoutInflater().inflate(R.layout.keypad_channel_switch, sceneContainer, false);
InputBannerView inputBannerView = (InputBannerView) getLayoutInflater()
.inflate(R.layout.input_banner, sceneContainer, false);
SelectInputView selectInputView = (SelectInputView) getLayoutInflater()
@@ -641,26 +591,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
});
mSearchFragment = new ProgramGuideSearchFragment();
- mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView,
- mKeypadChannelSwitchView, mChannelBannerView, inputBannerView,
+ mOverlayManager = new TvOverlayManager(this, mChannelTuner, mTvView, mTvOptionsManager,
+ keypadChannelSwitchView, channelBannerView, inputBannerView,
selectInputView, sceneContainer, mSearchFragment);
- mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
- mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
-
- mMediaSession = new MediaSession(this, MEDIA_SESSION_TAG);
- mMediaSession.setCallback(new MediaSession.Callback() {
- @Override
- public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
- // Consume the media button event here. Should not send it to other apps.
- return true;
- }
- });
- mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
- MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
- mNowPlayingCardWidth = getResources().getDimensionPixelSize(
- R.dimen.notif_card_img_max_width);
- mNowPlayingCardHeight = getResources().getDimensionPixelSize(R.dimen.notif_card_img_height);
+ mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
+ mMediaSessionWrapper = new MediaSessionWrapper(this);
mTvViewUiManager.restoreDisplayMode(false);
if (!handleIntent(getIntent())) {
@@ -668,15 +604,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
}
- mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(this,
- new AudioCapabilitiesReceiver.OnAc3PassthroughCapabilityChangeListener() {
- @Override
- public void onAc3PassthroughCapabilityChange(boolean capability) {
- mAc3PassthroughSupported = capability;
- }
- });
- mAudioCapabilitiesReceiver.register();
-
mAccessibilityManager =
(AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
mSendConfigInfoRecurringRunner = new RecurringRunner(this, TimeUnit.DAYS.toMillis(1),
@@ -692,6 +619,13 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mDvrConflictChecker = new ConflictChecker(this);
}
initForTest();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onCreate end");
+ mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONCREATE);
+ }
+
+ private void startOnboardingActivity() {
+ startActivity(OnboardingActivity.buildIntent(this, getIntent()));
+ finish();
}
@Override
@@ -705,32 +639,21 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
- switch (requestCode) {
- case PERMISSIONS_REQUEST_READ_TV_LISTINGS:
- if (grantResults.length > 0
- && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // Start reload of dependent data
- mChannelDataManager.reload();
- mProgramDataManager.reload();
-
- // Restart live channels.
- Intent intent = getIntent();
- finish();
- startActivity(intent);
- } else {
- Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
- Toast.LENGTH_LONG).show();
- finish();
- }
- break;
- 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();
- }
- break;
+ if (requestCode == PERMISSIONS_REQUEST_READ_TV_LISTINGS) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // Start reload of dependent data
+ mChannelDataManager.reload();
+ mProgramDataManager.reload();
+
+ // Restart live channels.
+ Intent intent = getIntent();
+ finish();
+ startActivity(intent);
+ } else {
+ Toast.makeText(this, R.string.msg_read_tv_listing_permission_denied,
+ Toast.LENGTH_LONG).show();
+ finish();
+ }
}
}
@@ -781,6 +704,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onStart() {
+ TimerEvent timer = mPerformanceMonitor.startTimer();
if (DEBUG) Log.d(TAG,"onStart()");
super.onStart();
mScreenOffIntentReceived = false;
@@ -789,23 +713,25 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mMainDurationTimer.start();
applyParentalControlSettings();
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(TvInputManager.ACTION_PARENTAL_CONTROLS_ENABLED_CHANGED);
- intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
- intentFilter.addAction(Intent.ACTION_SCREEN_ON);
- registerReceiver(mBroadcastReceiver, intentFilter);
+ registerReceiver(mBroadcastReceiver, SYSTEM_INTENT_FILTER);
- Intent notificationIntent = new Intent(this, NotificationService.class);
- notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
- startService(notificationIntent);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ Intent notificationIntent = new Intent(this, NotificationService.class);
+ notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
+ startService(notificationIntent);
+ }
+ TunerInputController.executeNetworkTunerDiscoveryAsyncTask(this);
+
+ EpgFetcher.getInstance(this).fetchImmediatelyIfNeeded();
+ mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONSTART);
}
@Override
protected void onResume() {
+ TimerEvent timer = mPerformanceMonitor.startTimer();
+ 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) {
@@ -819,23 +745,23 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mActivityResumed = true;
mShowNewSourcesFragment = true;
mOtherActivityLaunched = false;
- int result = mAudioManager.requestAudioFocus(MainActivity.this,
- AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
- mAudioFocusStatus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) ?
- AudioManager.AUDIOFOCUS_GAIN : AudioManager.AUDIOFOCUS_LOSS;
- setVolumeByAudioFocusStatus();
+ mAudioManagerHelper.requestAudioFocus();
if (mTvView.isPlaying()) {
// Every time onResume() is called the activity will be assumed to not have requested
// visible behind.
requestVisibleBehind(true);
}
- if (Utils.hasRecordingFailedReason(getApplicationContext(),
- TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)) {
+ Set<String> failedScheduledRecordingInfoSet =
+ Utils.getFailedScheduledRecordingInfoSet(getApplicationContext());
+ if (Utils.hasRecordingFailedReason(
+ getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE)
+ && !failedScheduledRecordingInfoSet.isEmpty()) {
runAfterAttachedToWindow(new Runnable() {
@Override
public void run() {
- DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this);
+ DvrUiHelper.showDvrInsufficientSpaceErrorDialog(MainActivity.this,
+ failedScheduledRecordingInfoSet);
}
});
}
@@ -843,13 +769,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mChannelTuner.areAllChannelsLoaded()) {
SetupUtils.getInstance(this).markNewChannelsBrowsable();
resumeTvIfNeeded();
- resumePipIfNeeded();
}
mOverlayManager.showMenuWithTimeShiftPauseIfNeeded();
- // Note: The following codes are related to pop up an overlay UI after resume.
- // When the following code is changed, please check the variable
- // willShowOverlayUiAfterResume in updateChannelBannerAndShowIfNeeded.
+ // NOTE: The following codes are related to pop up an overlay UI after resume. When
+ // the following code is changed, please modify willShowOverlayUiWhenResume() accordingly.
if (mInputToSetUp != null) {
startSetupActivity(mInputToSetUp, false);
mInputToSetUp = null;
@@ -881,6 +805,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mDvrConflictChecker != null) {
mDvrConflictChecker.start();
}
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity.onResume end");
+ mPerformanceMonitor.stopTimer(timer, EventNames.MAIN_ACTIVITY_ONRESUME);
}
@Override
@@ -893,18 +819,12 @@ 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;
if (!mVisibleBehind) {
- mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
- mAudioManager.abandonAudioFocus(this);
- if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
- }
+ mAudioManagerHelper.abandonAudioFocus();
+ mMediaSessionWrapper.setPlaybackState(false);
mTracker.sendScreenView("");
} else {
mTracker.sendScreenView(SCREEN_BEHIND_NAME);
@@ -933,6 +853,32 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return state;
}
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ if (checked) {
+ switch (type) {
+ case PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
+ blockOrUnblockScreen(mTvView, false);
+ mIsCurrentChannelUnblockedByUser = true;
+ break;
+ case PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
+ TvContentRating unblockedRating = TvContentRating.unflattenFromString(rating);
+ mLastAllowedRatingForCurrentChannel = unblockedRating;
+ mTvView.unblockContent(unblockedRating);
+ break;
+ case PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN:
+ mOverlayManager.getSideFragmentManager()
+ .show(new ParentalControlsFragment(), false);
+ // Pass through.
+ case PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN:
+ mOverlayManager.getSideFragmentManager().showSidePanel(true);
+ break;
+ }
+ } else if (type == PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN) {
+ mOverlayManager.getSideFragmentManager().hideAll(false);
+ }
+ }
+
private void resumeTvIfNeeded() {
if (DEBUG) Log.d(TAG, "resumeTvIfNeeded()");
if (!mTvView.isPlaying() || mInitChannelUri != null
@@ -962,21 +908,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))
@@ -993,7 +924,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// TV has already started.
if (channelUri == null || channelUri.equals(mChannelTuner.getCurrentChannelUri())) {
// Simply adjust the volume without tune.
- setVolumeByAudioFocusStatus();
+ mAudioManagerHelper.setVolumeByAudioFocusStatus();
return;
}
stopTv();
@@ -1027,9 +958,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- mTvView.start(mTvInputManagerHelper);
- setVolumeByAudioFocusStatus();
- tune();
+ mTvView.start();
+ mAudioManagerHelper.setVolumeByAudioFocusStatus();
+ tune(true);
}
@Override
@@ -1072,7 +1003,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() {
@@ -1127,10 +1057,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
public void startSystemCaptioningSettingsActivity() {
Intent intent = new Intent(Settings.ACTION_CAPTIONING_SETTINGS);
- mOverlayManager.hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_WITHOUT_ANIMATION
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANEL_HISTORY);
try {
- startActivityForResultSafe(intent, REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS);
+ startActivitySafe(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, getString(R.string.msg_unable_to_start_system_captioning_settings),
Toast.LENGTH_SHORT).show();
@@ -1145,10 +1073,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mProgramDataManager;
}
- public PipInputManager getPipInputManager() {
- return mPipInputManager;
- }
-
public TvOptionsManager getTvOptionsManager() {
return mTvOptionsManager;
}
@@ -1185,13 +1109,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
/**
- * Returns true if the current connected TV supports AC3 passthough.
- */
- public boolean isAc3PassthroughSupported() {
- return mAc3PassthroughSupported;
- }
-
- /**
* Returns the current program which the user is watching right now.<p>
*
* It might be a live program. If the time shifting is available, it can be a past program, too.
@@ -1218,8 +1135,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private Channel getBrowsableChannel() {
- // TODO: mChannelMap could be dirty for a while when the browsablity of channels
- // are changed. In that case, we shouldn't use the value from mChannelMap.
Channel curChannel = mChannelTuner.getCurrentChannel();
if (curChannel != null && curChannel.isBrowsable()) {
return curChannel;
@@ -1254,9 +1169,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// Show ChannelSourcesFragment only if all the channels are loaded.
return;
}
- Channel currentChannel = mChannelTuner.getCurrentChannel();
- long channelId = currentChannel == null ? Channel.INVALID_ID : currentChannel.getId();
- mOverlayManager.getSideFragmentManager().show(new SettingsFragment(channelId));
+ mOverlayManager.getSideFragmentManager().show(new SettingsFragment());
}
public void showMerchantCollection() {
@@ -1272,19 +1185,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 +1225,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;
- }
}
}
@@ -1344,37 +1241,41 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mTvViewUiManager.isUnderShrunkenTvView() || mIsCompletingShrunkenTvView;
}
+ /**
+ * Returns {@code true} if the tunable tv view is blocked by resource conflict or by parental
+ * control, otherwise {@code false}.
+ */
+ public boolean isScreenBlockedByResourceConflictOrParentalControl() {
+ return mTvView.getVideoUnavailableReason()
+ == TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE || mTvView.isBlocked();
+ }
+
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_CODE_START_SETUP_ACTIVITY:
- if (resultCode == RESULT_OK) {
- int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup);
- String text;
- if (count > 0) {
- text = getResources().getQuantityString(R.plurals.msg_channel_added,
- count, count);
- } else {
- text = getString(R.string.msg_no_channel_added);
- }
- Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
- mInputIdUnderSetup = null;
- if (mChannelTuner.getCurrentChannel() == null) {
- mChannelTuner.moveToAdjacentBrowsableChannel(true);
- }
- if (mTunePending) {
- tune();
- }
+ if (requestCode == REQUEST_CODE_START_SETUP_ACTIVITY) {
+ if (resultCode == RESULT_OK) {
+ int count = mChannelDataManager.getChannelCountForInput(mInputIdUnderSetup);
+ String text;
+ if (count > 0) {
+ text = getResources().getQuantityString(R.plurals.msg_channel_added,
+ count, count);
} else {
- mInputIdUnderSetup = null;
+ text = getString(R.string.msg_no_channel_added);
}
- if (!mIsSetupActivityCalledByPopup) {
- mOverlayManager.getSideFragmentManager().showSidePanel(false);
+ Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT).show();
+ mInputIdUnderSetup = null;
+ if (mChannelTuner.getCurrentChannel() == null) {
+ mChannelTuner.moveToAdjacentBrowsableChannel(true);
}
- break;
- case REQUEST_CODE_START_SYSTEM_CAPTIONING_SETTINGS:
+ if (mTunePending) {
+ tune(true);
+ }
+ } else {
+ mInputIdUnderSetup = null;
+ }
+ if (!mIsSetupActivityCalledByPopup) {
mOverlayManager.getSideFragmentManager().showSidePanel(false);
- break;
+ }
}
if (data != null) {
String errorMessage = data.getStringExtra(LauncherActivity.ERROR_MESSAGE);
@@ -1418,18 +1319,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
// while gamepads support DPAD_CENTER and BACK by fallback.
// Since we don't expect that TIS want to handle gamepad buttons now,
// blacklist gamepad buttons and wait for next fallback keys.
- // TODO) Need to consider other fallback keys (e.g. ESCAPE)
+ // TODO: Need to consider other fallback keys (e.g. ESCAPE)
return super.dispatchKeyEvent(event);
}
return dispatchKeyEventToSession(event) || super.dispatchKeyEvent(event);
}
- @Override
- public void onAudioFocusChange(int focusChange) {
- mAudioFocusStatus = focusChange;
- setVolumeByAudioFocusStatus();
- }
-
/**
* Notifies the key input focus is changed to the TV view.
*/
@@ -1449,18 +1344,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!mTvView.isPlaying()) {
mCaptionSettings = new CaptionSettings(this);
}
-
- // Handle the passed key press, if any. Note that only the key codes that are currently
- // handled in the TV app will be handled via Intent.
- // TODO: Consider defining a separate intent filter as passing data of mime type
- // vnd.android.cursor.item/channel isn't really necessary here.
- int keyCode = intent.getIntExtra(Utils.EXTRA_KEY_KEYCODE, KeyEvent.KEYCODE_UNKNOWN);
- if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
- if (DEBUG) Log.d(TAG, "Got an intent with keycode: " + keyCode);
- KeyEvent event = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
- onKeyUp(keyCode, event);
- return true;
- }
mShouldTuneToTunerChannel = intent.getBooleanExtra(Utils.EXTRA_KEY_FROM_LAUNCHER, false);
mInitChannelUri = null;
@@ -1476,9 +1359,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- // TODO: remove the checkState once N API is finalized.
- SoftPreconditions.checkState(TvInputManager.ACTION_SETUP_INPUTS.equals(
- "android.media.tv.action.SETUP_INPUTS"));
if (TvInputManager.ACTION_SETUP_INPUTS.equals(intent.getAction())) {
runAfterAttachedToWindow(new Runnable() {
@Override
@@ -1488,17 +1368,12 @@ 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".
- // Later, we might want to add handling of individual programs too.
if (Utils.isProgramsUri(uri)) {
- // The given data is a programs URI. Open the Program Guide.
+ // 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".
+ // Later, we might want to add handling of individual programs too.
mShowProgramGuide = true;
return true;
}
@@ -1541,17 +1416,19 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
} else if (mInitChannelUri != null) {
// Handle the URI built by TvContract.buildChannelsUriForInput().
- // TODO: Change hard-coded "input" to TvContract.PARAM_INPUT.
String inputId = mInitChannelUri.getQueryParameter("input");
long channelId = Utils.getLastWatchedChannelIdForInput(this, inputId);
if (channelId == Channel.INVALID_ID) {
String[] projection = { Channels._ID };
+ long time = System.currentTimeMillis();
try (Cursor cursor = getContentResolver().query(uri, projection,
null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
channelId = cursor.getLong(0);
}
}
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("MainActivity queries DB for "
+ + "last channel check (" + (System.currentTimeMillis() - time) + "ms)");
}
if (channelId == Channel.INVALID_ID) {
// Couldn't find any channel probably because the input hasn't been set up.
@@ -1567,41 +1444,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return true;
}
- 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()) {
- switch (mAudioFocusStatus) {
- case AudioManager.AUDIOFOCUS_GAIN:
- tvView.setStreamVolume(AUDIO_MAX_VOLUME);
- break;
- case AudioManager.AUDIOFOCUS_LOSS:
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
- tvView.setStreamVolume(AUDIO_MIN_VOLUME);
- break;
- case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
- tvView.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() {
stopTv(null, false);
}
@@ -1617,10 +1459,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!keepVisibleBehind) {
requestVisibleBehind(false);
}
- mAudioManager.abandonAudioFocus(this);
- if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
- }
+ mAudioManagerHelper.abandonAudioFocus();
+ mMediaSessionWrapper.setPlaybackState(false);
}
TvApplication.getSingletons(this).getMainActivityWrapper()
.notifyCurrentChannelChange(this, null);
@@ -1628,99 +1468,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mTunePending = false;
}
- private boolean isPlaying() {
- 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 +1487,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();
@@ -1748,6 +1500,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
mTunePending = false;
final Channel channel = mChannelTuner.getCurrentChannel();
+ SoftPreconditions.checkState(channel != null);
+ if (channel == null) {
+ return;
+ }
if (!mChannelTuner.isCurrentChannelPassthrough()) {
if (mTvInputManagerHelper.getTunerTvInputSize() == 0) {
Toast.makeText(this, R.string.msg_no_input, Toast.LENGTH_SHORT).show();
@@ -1763,23 +1519,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (mChannelDataManager.getChannelCount() > 0) {
mOverlayManager.showIntroDialog();
+ } else {
+ startOnboardingActivity();
+ return;
}
}
- if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
- && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
- // Show new channel sources fragment.
- runAfterAttachedToWindow(new Runnable() {
- @Override
- public void run() {
- mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
- @Override
- public void run() {
- mOverlayManager.showNewSourcesFragment();
- }
- });
- }
- });
- }
mShowNewSourcesFragment = false;
if (mChannelTuner.getBrowsableChannelCount() == 0
&& mChannelDataManager.getChannelCount() > 0
@@ -1791,24 +1535,24 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mOverlayManager.getSideFragmentManager().show(
new CustomizeChannelListFragment());
} else {
- showSettingsFragment();
+ mOverlayManager.showSetupFragment();
}
return;
}
- // TODO: need to refactor the following code to put in startTv.
- if (channel == null) {
- // There is no channel to tune to.
- stopTv("tune()", false);
- if (!mChannelDataManager.isDbLoadFinished()) {
- // Wait until channel data is loaded in order to know the number of channels.
- // tune() will be retried, once the channel data is loaded.
- return;
- }
- if (mOverlayManager.getSideFragmentManager().isActive()) {
- return;
- }
- mOverlayManager.showSetupFragment();
- return;
+ if (!TvCommonUtils.isRunningInTest() && mShowNewSourcesFragment
+ && setupUtils.hasUnrecognizedInput(mTvInputManagerHelper)) {
+ // Show new channel sources fragment.
+ runAfterAttachedToWindow(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.runAfterOverlaysAreClosed(new Runnable() {
+ @Override
+ public void run() {
+ mOverlayManager.showNewSourcesFragment();
+ }
+ });
+ }
+ });
}
setupUtils.onTuned();
if (mTuneParams != null) {
@@ -1825,7 +1569,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,15 +1595,21 @@ 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) {
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.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
// be called during the pause state by mBroadcastReceiver (Intent.ACTION_SCREEN_ON).
requestVisibleBehind(true);
}
- updateMediaSession();
+ mMediaSessionWrapper.update(mTvView.isBlocked(), getCurrentChannel(), getCurrentProgram());
}
// Runs the runnable after the activity is attached to window to show the fragment transition
@@ -1895,136 +1644,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- private void updateMediaSession() {
- if (getCurrentChannel() == null) {
- mMediaSession.setActive(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);
- return;
- }
-
- final Program currentProgram = getCurrentProgram();
- String cardTitleText = null;
- String posterArtUri = null;
- if (currentProgram != null) {
- cardTitleText = currentProgram.getTitle();
- posterArtUri = currentProgram.getPosterArtUri();
- }
- if (TextUtils.isEmpty(cardTitleText)) {
- cardTitleText = getCurrentChannelName();
- }
- updateMediaMetadata(cardTitleText, null);
- setMediaSessionPlaybackState(true);
-
- if (posterArtUri == null) {
- posterArtUri = TvContract.buildChannelLogoUri(getCurrentChannelId()).toString();
- }
- updatePosterArt(getCurrentChannel(), currentProgram, cardTitleText, null, posterArtUri);
- mMediaSession.setActive(true);
- }
-
- private void updatePosterArt(Channel currentChannel, Program currentProgram,
- String cardTitleText, @Nullable Bitmap posterArt, @Nullable String posterArtUri) {
- if (posterArt != null) {
- updateMediaMetadata(cardTitleText, posterArt);
- } else if (posterArtUri != null) {
- ImageLoader.loadBitmap(this, posterArtUri, mNowPlayingCardWidth, mNowPlayingCardHeight,
- new ProgramPosterArtCallback(this, currentChannel,
- currentProgram, cardTitleText));
- } else {
- updateMediaMetadata(cardTitleText, R.drawable.default_now_card);
- }
- }
-
- private static class ProgramPosterArtCallback extends
- ImageLoader.ImageLoaderCallback<MainActivity> {
- private final Channel mChannel;
- private final Program mProgram;
- private final String mCardTitleText;
-
- public ProgramPosterArtCallback(MainActivity mainActivity, Channel channel, Program program,
- String cardTitleText) {
- super(mainActivity);
- mChannel = channel;
- mProgram = program;
- mCardTitleText = cardTitleText;
- }
-
- @Override
- public void onBitmapLoaded(MainActivity mainActivity, @Nullable Bitmap posterArt) {
- if (mainActivity.isNowPlayingProgram(mChannel, mProgram)) {
- mainActivity.updatePosterArt(mChannel, mProgram, mCardTitleText, posterArt, null);
- }
- }
- }
-
- private boolean isNowPlayingProgram(Channel channel, Program program) {
+ boolean isNowPlayingProgram(Channel channel, Program program) {
return program == null ? (channel != null && getCurrentProgram() == null
&& channel.equals(getCurrentChannel())) : program.equals(getCurrentProgram());
}
- private void updateMediaMetadata(final String title, final Bitmap posterArt) {
- new AsyncTask<Void, Void, Void> () {
- @Override
- protected Void doInBackground(Void... arg0) {
- MediaMetadata.Builder builder = new MediaMetadata.Builder();
- builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
- if (posterArt != null) {
- builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
- }
- mMediaSession.setMetadata(builder.build());
- return null;
- }
- }.execute();
- }
-
- private void updateMediaMetadata(final String title, final int imageResId) {
- new AsyncTask<Void, Void, Void> () {
- @Override
- protected Void doInBackground(Void... arg0) {
- MediaMetadata.Builder builder = new MediaMetadata.Builder();
- builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
- Bitmap posterArt = BitmapFactory.decodeResource(getResources(), imageResId);
- if (posterArt != null) {
- builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
- }
- mMediaSession.setMetadata(builder.build());
- return null;
- }
- }.execute();
- }
-
- private String getCurrentChannelName() {
- Channel channel = getCurrentChannel();
- if (channel == null) {
- return "";
- }
- if (channel.isPassthrough()) {
- TvInputInfo input = getTvInputManagerHelper().getTvInputInfo(channel.getInputId());
- return Utils.loadLabel(this, input);
- } else {
- return channel.getDisplayName();
- }
- }
-
- 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 addToRecentChannels(long channelId) {
if (!mRecentChannels.remove(channelId)) {
if (mRecentChannels.size() >= MAX_RECENT_CHANNELS) {
@@ -2042,106 +1666,35 @@ 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();
+ private void blockOrUnblockScreen(TunableTvView tvView, boolean blockOrUnblock) {
+ tvView.blockOrUnblockScreen(blockOrUnblock);
if (tvView == mTvView) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
- updateMediaSession();
- }
- }
-
- private void unblockScreen(TunableTvView tvView) {
- tvView.unblockScreen();
- if (tvView == mTvView) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
- updateMediaSession();
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ mMediaSessionWrapper.update(blockOrUnblock, getCurrentChannel(), getCurrentProgram());
}
}
/**
- * Shows the channel banner if it was hidden from the side fragment.
- *
- * <p>When the side fragment is visible, showing the channel banner should be put off until the
- * side fragment is closed even though the channel changes.
- */
- public void showChannelBannerIfHiddenBySideFragment() {
- if (mChannelBannerHiddenBySideFragment) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
- }
- }
-
- private void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) {
- if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")");
- if (!mChannelTuner.isCurrentChannelPassthrough()) {
- int lockType = ChannelBannerView.LOCK_NONE;
- if (mTvView.isScreenBlocked()) {
- lockType = ChannelBannerView.LOCK_CHANNEL_INFO;
- } else if (mTvView.getBlockedContentRating() != null
- || (getParentalControlSettings().isParentalControlsEnabled()
- && !mTvView.isVideoAvailable())) {
- // 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)
- && reason == UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK) {
- return;
- }
-
- mChannelBannerView.updateViews(mTvView);
- }
- boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW
- || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
- || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST);
- boolean noOverlayUiWhenResume =
- mInputToSetUp == null && !mShowProgramGuide && !mShowSelectInputView;
- if (needToShowBanner && noOverlayUiWhenResume
- && mOverlayManager.getCurrentDialog() == null
- && !mOverlayManager.isSetupFragmentActive()
- && !mOverlayManager.isNewSourcesFragmentActive()) {
- if (mChannelTuner.getCurrentChannel() == null) {
- mChannelBannerHiddenBySideFragment = false;
- } else if (mOverlayManager.getSideFragmentManager().isActive()) {
- mChannelBannerHiddenBySideFragment = true;
- } else {
- mChannelBannerHiddenBySideFragment = false;
- mOverlayManager.showBanner();
- }
- }
- updateAvailabilityToast();
- }
-
- /**
* Hide the overlays when tuning to a channel from the menu (e.g. Channels).
*/
public void hideOverlaysForTune() {
@@ -2197,7 +1750,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 +1763,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
private void applyClosedCaption() {
List<TvTrackInfo> tracks = getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks == null) {
- mTvOptionsManager.onClosedCaptionsChanged(null);
+ mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
return;
}
@@ -2219,17 +1772,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 +1793,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,29 +1812,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);
- }
-
- /**
- * Pops up the KeypadChannelSwitchView with the given key input event.
- *
- * @param keyCode A key code of the key event.
- */
- public void showKeypadChannelSwitchView(int keyCode) {
- if (mChannelTuner.areAllChannelsLoaded()) {
- mOverlayManager.showKeypadChannelSwitch();
- mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
- }
- }
-
- 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();
+ mTvOptionsManager.onClosedCaptionsChanged(null, UNDEFINED_TRACK_INDEX);
}
public void showProgramGuideSearchFragment() {
@@ -2295,13 +1834,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
@Override
protected void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy()");
- SideFragment.releasePreloadedRecycledViews();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).reset();
+ SideFragment.releaseRecycledViewPool();
+ ViewCache.getInstance().clear();
if (mTvView != null) {
mTvView.release();
}
- if (mPipView != null) {
- mPipView.release();
- }
if (mChannelTuner != null) {
mChannelTuner.removeListener(mChannelTunerListener);
mChannelTuner.stop();
@@ -2314,21 +1852,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mProgramDataManager.setPrefetchEnabled(false);
}
}
- if (mPipInputManager != null) {
- mPipInputManager.stop();
- }
if (mOverlayManager != null) {
mOverlayManager.release();
}
- if (mKeypadChannelSwitchView != null) {
- mKeypadChannelSwitchView.setChannels(null);
- }
mMemoryManageables.clear();
- if (mMediaSession != null) {
- mMediaSession.release();
+ if (mMediaSessionWrapper != null) {
+ mMediaSessionWrapper.release();
}
- if (mAudioCapabilitiesReceiver != null) {
- mAudioCapabilitiesReceiver.unregister();
+ if (mAudioManagerHelper != null) {
+ mAudioManagerHelper.release();
}
mHandler.removeCallbacksAndMessages(null);
application.getMainActivityWrapper().onMainActivityDestroyed(this);
@@ -2340,8 +1872,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 +1945,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 +1956,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:
@@ -2467,12 +2002,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
} else {
if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
- showKeypadChannelSwitchView(keyCode);
+ mOverlayManager.showKeypadChannelSwitch(keyCode);
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
- if (!mTvView.isVideoAvailable()
+ if (!mTvView.isVideoOrAudioAvailable()
&& mTvView.getVideoUnavailableReason()
== TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE) {
DvrUiHelper.startSchedulesActivityForTuneConflict(this,
@@ -2480,35 +2015,16 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return true;
}
if (!PermissionUtils.hasModifyParentalControls(this)) {
- // TODO: support this feature for non-system LC app. b/23939816
return true;
}
PinDialogFragment dialog = null;
if (mTvView.isScreenBlocked()) {
- dialog = new PinDialogFragment(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- if (success) {
- unblockScreen(mTvView);
- mIsCurrentChannelUnblockedByUser = true;
- }
- }
- });
- } else if (mTvView.getBlockedContentRating() != null) {
- final TvContentRating rating = mTvView.getBlockedContentRating();
- dialog = new PinDialogFragment(
- PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- if (success) {
- mLastAllowedRatingForCurrentChannel = rating;
- mTvView.unblockContent(rating);
- }
- }
- });
+ dialog = PinDialogFragment
+ .create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_CHANNEL);
+ } else if (mTvView.isContentBlocked()) {
+ dialog = PinDialogFragment
+ .create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+ mTvView.getBlockedContentRating().flattenToString());
}
if (dialog != null) {
mOverlayManager.showDialogFragment(PinDialogFragment.DIALOG_TAG, dialog,
@@ -2531,7 +2047,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return true;
}
if (keyCode != KeyEvent.KEYCODE_MENU) {
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
}
if (keyCode != KeyEvent.KEYCODE_E) {
mOverlayManager.showMenu(Menu.REASON_NONE);
@@ -2547,6 +2064,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
+ // Pass through.
case KeyEvent.KEYCODE_CAPTIONS: {
mOverlayManager.getSideFragmentManager().show(new ClosedCaptionFragment());
return true;
@@ -2555,14 +2073,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!SystemProperties.USE_DEBUG_KEYS.getValue()) {
break;
}
+ // Pass through.
case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
mOverlayManager.getSideFragmentManager().show(new MultiAudioFragment());
return true;
}
- case KeyEvent.KEYCODE_GUIDE: {
- mOverlayManager.showProgramGuide();
- return true;
- }
case KeyEvent.KEYCODE_INFO: {
mOverlayManager.showBanner();
return true;
@@ -2578,22 +2093,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(),
@@ -2624,7 +2134,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
if (SystemProperties.USE_DEBUG_KEYS.getValue() || BuildConfig.ENG) {
switch (keyCode) {
- case KeyEvent.KEYCODE_W: {
+ case KeyEvent.KEYCODE_W:
mDebugNonFullSizeScreen = !mDebugNonFullSizeScreen;
if (mDebugNonFullSizeScreen) {
FrameLayout.LayoutParams params =
@@ -2632,30 +2142,23 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
params.width = 960;
params.height = 540;
params.gravity = Gravity.START;
- mTvView.setLayoutParams(params);
+ mTvView.setTvViewLayoutParams(params);
} else {
FrameLayout.LayoutParams params =
(FrameLayout.LayoutParams) mTvView.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER;
- mTvView.setLayoutParams(params);
+ mTvView.setTvViewLayoutParams(params);
}
return true;
- }
- case KeyEvent.KEYCODE_P: {
- togglePipView();
- return true;
- }
case KeyEvent.KEYCODE_CTRL_LEFT:
- case KeyEvent.KEYCODE_CTRL_RIGHT: {
+ case KeyEvent.KEYCODE_CTRL_RIGHT:
mUseKeycodeBlacklist = !mUseKeycodeBlacklist;
return true;
- }
- case KeyEvent.KEYCODE_O: {
+ case KeyEvent.KEYCODE_O:
mOverlayManager.getSideFragmentManager().show(new DisplayModeFragment());
return true;
- }
case KeyEvent.KEYCODE_D:
mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment());
return true;
@@ -2681,22 +2184,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
@Override
- public void onBackPressed() {
- // The activity should be returned to the caller of this activity
- // when the mSource is not null.
- if (!mOverlayManager.getSideFragmentManager().isActive() && isPlaying()
- && mSource == null) {
- // If back key would exit TV app,
- // show McLauncher instead so we can get benefit of McLauncher's shyMode.
- Intent startMain = new Intent(Intent.ACTION_MAIN);
- startMain.addCategory(Intent.CATEGORY_HOME);
- startActivity(startMain);
- } else {
- super.onBackPressed();
- }
- }
-
- @Override
public void onUserInteraction() {
super.onUserInteraction();
if (mOverlayManager != null) {
@@ -2725,66 +2212,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
}
- public void togglePipView() {
- enablePipView(!mPipEnabled, true);
- mOverlayManager.getMenu().update();
- }
-
- public boolean isPipEnabled() {
- return mPipEnabled;
- }
-
- public void tuneToChannelForPip(Channel channel) {
- if (!mPipEnabled) {
- throw new IllegalStateException("tuneToChannelForPip is called when PIP is off");
- }
- if (mPipChannel.equals(channel)) {
- return;
- }
- mPipChannel = channel;
- startPip(true);
- }
-
- private void enablePipView(boolean enable, boolean fromUserInteraction) {
- if (enable == mPipEnabled) {
- return;
- }
- if (enable) {
- List<PipInput> pipAvailableInputs = mPipInputManager.getPipInputList(true);
- if (pipAvailableInputs.isEmpty()) {
- Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
- .show();
- return;
- }
- // TODO: choose the last pip input.
- Channel pipChannel = pipAvailableInputs.get(0).getChannel();
- if (pipChannel != null) {
- mPipEnabled = true;
- mPipChannel = pipChannel;
- startPip(fromUserInteraction);
- mTvViewUiManager.restorePipSize();
- mTvViewUiManager.restorePipLayout();
- mTvOptionsManager.onPipChanged(mPipEnabled);
- } else {
- Toast.makeText(this, R.string.msg_no_available_input_by_pip, Toast.LENGTH_SHORT)
- .show();
- }
- } else {
- mPipEnabled = false;
- mPipChannel = null;
- // Recover the stream volume of the main TV view, if needed.
- if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
- setVolumeByAudioFocusStatus(mTvView);
- mPipSound = TvSettings.PIP_SOUND_MAIN;
- mTvOptionsManager.onPipSoundChanged(mPipSound);
- }
- stopPip();
- mTvViewUiManager.restoreDisplayMode(false);
- mTvOptionsManager.onPipChanged(mPipEnabled);
- }
- }
-
- private boolean isChannelChangeKeyDownReceived() {
+ /**
+ * Returns {@code true} if one of the channel changing keys are pressed and not released yet.
+ */
+ public boolean isChannelChangeKeyDownReceived() {
return mHandler.hasMessages(MSG_CHANNEL_UP_PRESSED)
|| mHandler.hasMessages(MSG_CHANNEL_DOWN_PRESSED);
}
@@ -2811,10 +2242,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 +2259,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 +2278,17 @@ 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);
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.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();
}
@@ -2883,107 +2305,25 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
*/
private void moveToAdjacentChannel(boolean channelUp, boolean fastTuning) {
if (mChannelTuner.moveToAdjacentBrowsableChannel(channelUp)) {
- updateChannelBannerAndShowIfNeeded(fastTuning ? UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST
- : UPDATE_CHANNEL_BANNER_REASON_TUNE);
- }
- }
-
- 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();
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(fastTuning ?
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST
+ : TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
-
- 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
public void onVisibleBehindCanceled() {
stopTv("onVisibleBehindCanceled()", false);
mTracker.sendScreenView("");
- mAudioFocusStatus = AudioManager.AUDIOFOCUS_LOSS;
- mAudioManager.abandonAudioFocus(this);
- if (mMediaSession.isActive()) {
- mMediaSession.setActive(false);
- }
- stopPip();
+ mAudioManagerHelper.abandonAudioFocus();
+ mMediaSessionWrapper.setPlaybackState(false);
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 +2352,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);
}
}
@@ -3073,16 +2413,12 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
private void updateAvailabilityToast() {
- updateAvailabilityToast(mTvView);
- }
-
- private void updateAvailabilityToast(StreamInfo info) {
- if (info.isVideoAvailable()) {
+ if (mTvView.isVideoAvailable()
+ || mTvView.getCurrentChannel() != mChannelTuner.getCurrentChannel()) {
return;
}
- int stringId;
- switch (info.getVideoUnavailableReason()) {
+ switch (mTvView.getVideoUnavailableReason()) {
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
case TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
@@ -3092,13 +2428,22 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return;
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
default:
- stringId = R.string.msg_channel_unavailable_unknown;
+ Toast.makeText(this, R.string.msg_channel_unavailable_unknown,
+ Toast.LENGTH_SHORT).show();
break;
}
+ }
- Toast.makeText(this, stringId, Toast.LENGTH_SHORT).show();
+ /**
+ * Returns {@code true} if some overlay UI will be shown when the activity is resumed.
+ */
+ public boolean willShowOverlayUiWhenResume() {
+ return mInputToSetUp != null || mShowProgramGuide || mShowSelectInputView;
}
+ /**
+ * Returns the current parental control settings.
+ */
public ParentalControlSettings getParentalControlSettings() {
return mTvInputManagerHelper.getParentalControlSettings();
}
@@ -3110,6 +2455,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
return mTvInputManagerHelper.getContentRatingsManager();
}
+ /**
+ * Returns the current captioning settings.
+ */
public CaptionSettings getCaptionSettings() {
return mCaptionSettings;
}
@@ -3163,6 +2511,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mActivityStarted) {
initAnimations();
initSideFragments();
+ initMenuItemViews();
}
}
}, LAZY_INITIALIZATION_DELAY);
@@ -3174,7 +2523,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 +2560,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 +2574,10 @@ 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;
}
@@ -3249,7 +2594,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (mTvView.isFadedOut()) {
mTvView.removeFadeEffect();
}
- // TODO: show something to user about this error.
+ Toast.makeText(MainActivity.this, R.string.msg_channel_unavailable_unknown,
+ Toast.LENGTH_SHORT).show();
}
@Override
@@ -3258,29 +2604,21 @@ 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()) {
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO);
}
-
applyDisplayRefreshRate(info.getVideoFrameRate());
- mTvViewUiManager.updateTvView();
+ mTvViewUiManager.updateTvAspectRatio();
applyMultiAudio();
applyClosedCaption();
- // TODO: Send command to TIS with checking the settings in TV and CaptionManager.
mOverlayManager.getMenu().onStreamInfoChanged();
if (mTvView.isVideoAvailable()) {
mTvViewUiManager.fadeInTvView();
}
+ if (!mTvView.isContentBlocked() && !mTvView.isScreenBlocked()) {
+ updateAvailabilityToast();
+ }
mHandler.removeCallbacks(mRestoreMainViewRunnable);
restoreMainTvView();
}
@@ -3303,11 +2641,15 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
}
mChannelTuner.setCurrentChannel(currentChannel);
mTvView.setCurrentChannel(currentChannel);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_TUNE);
+ mOverlayManager.updateChannelBannerAndShowIfNeeded(
+ TvOverlayManager.UPDATE_CHANNEL_BANNER_REASON_TUNE);
}
@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,9 +2661,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView();
mTvView.unblockContent(rating);
}
- mChannelBannerView.setBlockingContentRating(rating);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ mOverlayManager.setBlockingContentRating(rating);
mTvViewUiManager.fadeInTvView();
+ mMediaSessionWrapper.update(true, getCurrentChannel(), getCurrentProgram());
}
@Override
@@ -3329,8 +2671,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC
if (!isUnderShrunkenTvView()) {
mUnlockAllowedRatingBeforeShrunken = false;
}
- mChannelBannerView.setBlockingContentRating(null);
- updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ mOverlayManager.setBlockingContentRating(null);
+ mMediaSessionWrapper.update(false, getCurrentChannel(), getCurrentProgram());
}
}
}
diff --git a/src/com/android/tv/MainActivityWrapper.java b/src/com/android/tv/MainActivityWrapper.java
index 01733255..5af5079f 100644
--- a/src/com/android/tv/MainActivityWrapper.java
+++ b/src/com/android/tv/MainActivityWrapper.java
@@ -61,7 +61,7 @@ public final class MainActivityWrapper {
* Unsets the main activity instance.
*/
public void onMainActivityDestroyed(@NonNull MainActivity activity) {
- if (mActivity != activity) {
+ if (mActivity == activity) {
mActivity = null;
}
}
diff --git a/src/com/android/tv/MediaSessionWrapper.java b/src/com/android/tv/MediaSessionWrapper.java
new file mode 100644
index 00000000..da6ad2a4
--- /dev/null
+++ b/src/com/android/tv/MediaSessionWrapper.java
@@ -0,0 +1,216 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.data.Channel;
+import com.android.tv.data.Program;
+import com.android.tv.util.ImageLoader;
+import com.android.tv.util.Utils;
+
+/**
+ * A wrapper class for {@link MediaSession} to support common operations on media sessions for
+ * {@link MainActivity}.
+ */
+class MediaSessionWrapper {
+ private static final String MEDIA_SESSION_TAG = "com.android.tv.mediasession";
+ 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 final Context mContext;
+ private final MediaSession mMediaSession;
+ private int mNowPlayingCardWidth;
+ private int mNowPlayingCardHeight;
+
+ MediaSessionWrapper(Context context) {
+ mContext = context;
+ mMediaSession = new MediaSession(context, MEDIA_SESSION_TAG);
+ mMediaSession.setCallback(new MediaSession.Callback() {
+ @Override
+ public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
+ // Consume the media button event here. Should not send it to other apps.
+ return true;
+ }
+ });
+ mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
+ MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mNowPlayingCardWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.notif_card_img_max_width);
+ mNowPlayingCardHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.notif_card_img_height);
+ }
+
+ /**
+ * Sets playback state.
+ *
+ * @param isPlaying {@code true} if TV is playing, otherwise {@code false}.
+ */
+ void setPlaybackState(boolean isPlaying) {
+ if (isPlaying) {
+ mMediaSession.setActive(true);
+ // setPlaybackState() has to be called after calling setActive(). b/31933276
+ mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_PLAYING);
+ } else if (mMediaSession.isActive()) {
+ mMediaSession.setPlaybackState(MEDIA_SESSION_STATE_STOPPED);
+ mMediaSession.setActive(false);
+ }
+ }
+
+ /**
+ * Updates media session according to the current TV playback status.
+ *
+ * @param blocked {@code true} if the current channel is blocked, either by user settings or
+ * the current program's content ratings.
+ * @param currentChannel The currently playing channel.
+ * @param currentProgram The currently playing program.
+ */
+ void update(boolean blocked, Channel currentChannel, Program currentProgram) {
+ if (currentChannel == null) {
+ setPlaybackState(false);
+ return;
+ }
+
+ // If the channel is blocked, display a lock and a short text on the Now Playing Card
+ if (blocked) {
+ Bitmap art = BitmapFactory.decodeResource(mContext.getResources(),
+ R.drawable.ic_message_lock_preview);
+ updateMediaMetadata(mContext.getResources()
+ .getString(R.string.channel_banner_locked_channel_title), art);
+ setPlaybackState(true);
+ return;
+ }
+
+ String cardTitleText = null;
+ String posterArtUri = null;
+ if (currentProgram != null) {
+ cardTitleText = currentProgram.getTitle();
+ posterArtUri = currentProgram.getPosterArtUri();
+ }
+ if (TextUtils.isEmpty(cardTitleText)) {
+ cardTitleText = getChannelName(currentChannel);
+ }
+ updateMediaMetadata(cardTitleText, null);
+ if (posterArtUri == null) {
+ posterArtUri = TvContract.buildChannelLogoUri(currentChannel.getId()).toString();
+ }
+ updatePosterArt(currentChannel, currentProgram, cardTitleText, null, posterArtUri);
+ setPlaybackState(true);
+ }
+
+ /**
+ * Releases the media session.
+ *
+ * @see MediaSession#release()
+ */
+ void release() {
+ mMediaSession.release();
+ }
+
+ private String getChannelName(Channel channel) {
+ if (channel.isPassthrough()) {
+ TvInputInfo input = TvApplication.getSingletons(mContext).getTvInputManagerHelper()
+ .getTvInputInfo(channel.getInputId());
+ return Utils.loadLabel(mContext, input);
+ } else {
+ return channel.getDisplayName();
+ }
+ }
+
+ private void updatePosterArt(Channel currentChannel, Program currentProgram,
+ String cardTitleText, @Nullable Bitmap posterArt, @Nullable String posterArtUri) {
+ if (posterArt != null) {
+ updateMediaMetadata(cardTitleText, posterArt);
+ } else if (posterArtUri != null) {
+ ImageLoader.loadBitmap(mContext, posterArtUri, mNowPlayingCardWidth,
+ mNowPlayingCardHeight, new ProgramPosterArtCallback(this, currentChannel,
+ currentProgram, cardTitleText));
+ } else {
+ updateMediaMetadata(cardTitleText, R.drawable.default_now_card);
+ }
+ }
+
+ private void updateMediaMetadata(final String title, final Bitmap posterArt) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
+ if (posterArt != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
+ }
+ mMediaSession.setMetadata(builder.build());
+ return null;
+ }
+ }.execute();
+ }
+
+ private void updateMediaMetadata(final String title, final int imageResId) {
+ new AsyncTask<Void, Void, Void> () {
+ @Override
+ protected Void doInBackground(Void... arg0) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
+ Bitmap posterArt =
+ BitmapFactory.decodeResource(mContext.getResources(), imageResId);
+ if (posterArt != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
+ }
+ mMediaSession.setMetadata(builder.build());
+ return null;
+ }
+ }.execute();
+ }
+
+ private static class ProgramPosterArtCallback extends
+ ImageLoader.ImageLoaderCallback<MediaSessionWrapper> {
+ private final Channel mChannel;
+ private final Program mProgram;
+ private final String mCardTitleText;
+
+ ProgramPosterArtCallback(MediaSessionWrapper sessionWrapper, Channel channel,
+ Program program, String cardTitleText) {
+ super(sessionWrapper);
+ mChannel = channel;
+ mProgram = program;
+ mCardTitleText = cardTitleText;
+ }
+
+ @Override
+ public void onBitmapLoaded(MediaSessionWrapper sessionWrapper, @Nullable Bitmap posterArt) {
+ if (((MainActivity) sessionWrapper.mContext).isNowPlayingProgram(mChannel, mProgram)) {
+ sessionWrapper.updatePosterArt(mChannel, mProgram, mCardTitleText, posterArt, null);
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/SetupPassthroughActivity.java b/src/com/android/tv/SetupPassthroughActivity.java
index 8a263a26..f0f54413 100644
--- a/src/com/android/tv/SetupPassthroughActivity.java
+++ b/src/com/android/tv/SetupPassthroughActivity.java
@@ -18,19 +18,27 @@ package com.android.tv;
import android.app.Activity;
import android.content.ActivityNotFoundException;
+import android.content.Context;
import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.MainThread;
import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvCommonConstants;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.ChannelDataManager.Listener;
import com.android.tv.data.epg.EpgFetcher;
import com.android.tv.experiments.Experiments;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.util.concurrent.TimeUnit;
+
/**
* An activity to launch a TV input setup activity.
*
@@ -42,66 +50,83 @@ public class SetupPassthroughActivity extends Activity {
private static final int REQUEST_START_SETUP_ACTIVITY = 200;
+ private static ScanTimeoutMonitor sScanTimeoutMonitor;
+
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) {
+ if (sScanTimeoutMonitor == null) {
+ sScanTimeoutMonitor = new ScanTimeoutMonitor(this);
+ }
+ sScanTimeoutMonitor.startMonitoring();
+ EpgFetcher.getInstance(this).onChannelScanStarted();
+ }
}
- super.onDestroy();
}
@Override
public void onActivityResult(int requestCode, final int resultCode, final Intent data) {
+ if (DEBUG) Log.d(TAG, "onActivityResult");
+ if (sScanTimeoutMonitor != null) {
+ sScanTimeoutMonitor.stopMonitoring();
+ }
+ // Note: It's not guaranteed that this method is always called after scanning.
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();
+ }
if (!setupComplete) {
setResult(resultCode, data);
finish();
@@ -122,4 +147,75 @@ public class SetupPassthroughActivity extends Activity {
}
});
}
+
+ /**
+ * Monitors the scan progress and notifies the timeout of the scanning.
+ * The purpose of this monitor is to call EpgFetcher.onChannelScanFinished() in case when
+ * SetupPassthroughActivity.onActivityResult() is not called properly. b/36008534
+ */
+ @MainThread
+ private static class ScanTimeoutMonitor {
+ // Set timeout long enough. The message in Sony TV says the scanning takes about 30 minutes.
+ private static final long SCAN_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(30);
+
+ private final Context mContext;
+ private final ChannelDataManager mChannelDataManager;
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final Runnable mScanTimeoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ Log.w(TAG, "No channels has been added for a while." +
+ " The scan might have finished unexpectedly.");
+ onScanTimedOut();
+ }
+ };
+ private final Listener mChannelDataManagerListener = new Listener() {
+ @Override
+ public void onLoadFinished() {
+ setupTimer();
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ setupTimer();
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
+ private boolean mStarted;
+
+ private ScanTimeoutMonitor(Context context) {
+ mContext = context.getApplicationContext();
+ mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
+ }
+
+ private void startMonitoring() {
+ if (!mStarted) {
+ mStarted = true;
+ mChannelDataManager.addListener(mChannelDataManagerListener);
+ }
+ if (mChannelDataManager.isDbLoadFinished()) {
+ setupTimer();
+ }
+ }
+
+ private void stopMonitoring() {
+ if (mStarted) {
+ mStarted = false;
+ mHandler.removeCallbacks(mScanTimeoutRunnable);
+ mChannelDataManager.removeListener(mChannelDataManagerListener);
+ }
+ }
+
+ private void setupTimer() {
+ mHandler.removeCallbacks(mScanTimeoutRunnable);
+ mHandler.postDelayed(mScanTimeoutRunnable, SCAN_TIMEOUT_MS);
+ }
+
+ private void onScanTimedOut() {
+ stopMonitoring();
+ EpgFetcher.getInstance(mContext).onChannelScanFinished();
+ }
+ }
}
diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java
index 2d6d45c4..70885936 100644
--- a/src/com/android/tv/TimeShiftManager.java
+++ b/src/com/android/tv/TimeShiftManager.java
@@ -161,7 +161,6 @@ public class TimeShiftManager {
@TimeShiftActionId
private int mLastActionId = 0;
- // TODO: Remove these variables once API level 23 is available.
private final Context mContext;
private Program mCurrentProgram;
@@ -618,6 +617,15 @@ public class TimeShiftManager {
+ mAvailablityChangedTimeMs);
return;
}
+ if (recordStartTimeMs > System.currentTimeMillis()) {
+ // The time reported by TvInputService might not consistent with system
+ // clock,, use system's current time instead.
+ Log.e(TAG, "The start time should not be earlier than the current time, "
+ + "reset the start time to the system's current time: {"
+ + "startTime: " + recordStartTimeMs + ", current time: "
+ + System.currentTimeMillis());
+ recordStartTimeMs = System.currentTimeMillis();
+ }
if (mRecordStartTimeMs == recordStartTimeMs) {
return;
}
@@ -887,10 +895,12 @@ 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);
+ removeOutdatedPrograms(fetchStartTimeMs);
+ boolean needToLoad = addDummyPrograms(fetchStartTimeMs, fetchEndTimeMs);
if (needToLoad) {
- Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs);
+ Range<Long> period = Range.create(fetchStartTimeMs, fetchEndTimeMs);
mProgramLoadQueue.add(period);
startTaskIfNeeded();
}
@@ -996,6 +1006,12 @@ public class TimeShiftManager {
return added;
}
+ private void removeOutdatedPrograms(long startTimeMs) {
+ while (mPrograms.size() > 0 && mPrograms.get(0).getEndTimeUtcMillis() <= startTimeMs) {
+ mPrograms.remove(0);
+ }
+ }
+
private void removeDummyPrograms() {
for (Iterator<Program> it = mPrograms.listIterator(); it.hasNext(); ) {
if (!it.next().isValid()) {
@@ -1012,7 +1028,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..0c7c0fd1 100644
--- a/src/com/android/tv/TvApplication.java
+++ b/src/com/android/tv/TvApplication.java
@@ -22,16 +22,20 @@ import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
@@ -49,19 +53,30 @@ import com.android.tv.common.ui.setup.animation.SetupAnimationHelper;
import com.android.tv.config.DefaultConfigManager;
import com.android.tv.config.RemoteConfig;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.PreviewDataManager;
import com.android.tv.data.ProgramDataManager;
+import com.android.tv.data.epg.EpgFetcher;
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.RecordingScheduler;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.StubPerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
+import com.android.tv.recommendation.ChannelPreviewUpdater;
+import com.android.tv.recommendation.RecordedProgramPreviewUpdater;
+import com.android.tv.tuner.TunerInputController;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import com.android.tv.tuner.util.TunerInputInfoUtils;
import com.android.tv.util.AccountHelper;
import com.android.tv.util.Clock;
+import com.android.tv.util.Debug;
+import com.android.tv.util.PermissionUtils;
import com.android.tv.util.SetupUtils;
import com.android.tv.util.SystemProperties;
import com.android.tv.util.TvInputManagerHelper;
@@ -72,17 +87,25 @@ import java.util.List;
public class TvApplication extends Application implements ApplicationSingletons {
private static final String TAG = "TvApplication";
private static final boolean DEBUG = false;
- private RemoteConfig mRemoteConfig;
+ private static final TimerEvent sAppStartTimer = StubPerformanceMonitor.startBootstrapTimer();
+
+ /**
+ * An instance of {@link ApplicationSingletons}. Note that this can be set directly only for the
+ * test purpose.
+ */
+ @VisibleForTesting
+ public static ApplicationSingletons sAppSingletons;
/**
* Broadcast Action: The user has updated LC to a new version that supports tuner input.
- * {@link TunerInputController} will recevice this intent to check the existence of tuner
- * input when the new version is first launched.
+ * {@link com.android.tv.tuner.TunerInputController} will recevice this intent to check
+ * the existence of tuner input when the new version is first launched.
*/
public static final String ACTION_APPLICATION_FIRST_LAUNCHED =
"com.android.tv.action.APPLICATION_FIRST_LAUNCHED";
private static final String PREFERENCE_IS_FIRST_LAUNCH = "is_first_launch";
+ private RemoteConfig mRemoteConfig;
private String mVersionName = "";
private final MainActivityWrapper mMainActivityWrapper = new MainActivityWrapper();
@@ -92,21 +115,30 @@ public class TvApplication extends Application implements ApplicationSingletons
private Tracker mTracker;
private TvInputManagerHelper mTvInputManagerHelper;
private ChannelDataManager mChannelDataManager;
- private ProgramDataManager mProgramDataManager;
+ private volatile ProgramDataManager mProgramDataManager;
+ private PreviewDataManager mPreviewDataManager;
private DvrManager mDvrManager;
private DvrScheduleManager mDvrScheduleManager;
private DvrDataManager mDvrDataManager;
private DvrStorageStatusManager mDvrStorageStatusManager;
private DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private RecordingScheduler mRecordingScheduler;
@Nullable
private InputSessionManager mInputSessionManager;
private AccountHelper mAccountHelper;
// When this variable is null, we don't know in which process TvApplication runs.
private Boolean mRunningInMainProcess;
+ private PerformanceMonitor mPerformanceMonitor;
@Override
public void onCreate() {
super.onCreate();
+ if (!PermissionUtils.hasInternet(this)) {
+ // When an isolated process starts, just skip all the initialization.
+ return;
+ }
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).start();
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log("Start TvApplication.onCreate");
SharedPreferencesUtils.initialize(this, new Runnable() {
@Override
public void run() {
@@ -127,18 +159,15 @@ public class TvApplication extends Application implements ApplicationSingletons
}
Log.i(TAG, "Starting Live TV " + getVersionName());
-
// Only set StrictMode for ENG builds because the build server only produces userdebug
// builds.
if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) {
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog();
StrictMode.VmPolicy.Builder vmPolicyBuilder =
- new StrictMode.VmPolicy.Builder().detectAll().penaltyLog();
+ new StrictMode.VmPolicy.Builder().detectAll().penaltyDeath();
if (!TvCommonUtils.isRunningInTest()) {
threadPolicyBuilder.penaltyDialog();
- // Turn off death penalty for tests b/23355898
- vmPolicyBuilder.penaltyDeath();
}
StrictMode.setThreadPolicy(threadPolicyBuilder.build());
StrictMode.setVmPolicy(vmPolicyBuilder.build());
@@ -149,13 +178,16 @@ 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("finish TvApplication.onCreate");
+ getPerformanceMonitor().stopTimer(sAppStartTimer, EventNames.APPLICATION_ONCREATE);
}
private void setCurrentRunningProcess(boolean isMainProcess) {
@@ -163,12 +195,22 @@ public class TvApplication extends Application implements ApplicationSingletons
SoftPreconditions.checkState(isMainProcess == mRunningInMainProcess);
return;
}
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "start TvApplication.setCurrentRunningProcess");
mRunningInMainProcess = isMainProcess;
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess);
}
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ // Fetch remote config
+ getRemoteConfig().fetch(null);
+ return null;
+ }
+ }.execute();
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,
@@ -186,15 +228,22 @@ public class TvApplication extends Application implements ApplicationSingletons
if (Features.TUNER.isEnabled(this)) {
// If the tuner input service is added before the app is started, we need to
// handle it here.
- TunerInputInfoUtils.updateTunerInputInfo(this);
+ TunerInputInfoUtils.updateTunerInputInfo(TvApplication.this);
}
if (CommonFeatures.DVR.isEnabled(this)) {
mDvrScheduleManager = new DvrScheduleManager(this);
mDvrManager = new DvrManager(this);
- //NOTE: DvrRecordingService just keeps running.
- DvrRecordingService.startService(this);
+ mRecordingScheduler = RecordingScheduler.createScheduler(this);
+ }
+ EpgFetcher.getInstance(this).startRoutineService();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ChannelPreviewUpdater.getInstance(this).startRoutineService();
+ RecordedProgramPreviewUpdater.getInstance(this)
+ .updatePreviewDataForRecordedPrograms();
}
}
+ Debug.getTimer(Debug.TAG_START_UP_TIMER).log(
+ "finish TvApplication.setCurrentRunningProcess");
}
private void checkTunerServiceOnFirstLaunch() {
@@ -203,7 +252,7 @@ public class TvApplication extends Application implements ApplicationSingletons
boolean isFirstLaunch = sharedPreferences.getBoolean(PREFERENCE_IS_FIRST_LAUNCH, true);
if (isFirstLaunch) {
if (DEBUG) Log.d(TAG, "Congratulations, it's the first launch!");
- sendBroadcast(new Intent(ACTION_APPLICATION_FIRST_LAUNCHED));
+ TunerInputController.onCheckingUsbTunerStatus(this, ACTION_APPLICATION_FIRST_LAUNCHED);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(PREFERENCE_IS_FIRST_LAUNCH, false);
editor.apply();
@@ -227,6 +276,15 @@ public class TvApplication extends Application implements ApplicationSingletons
}
/**
+ * Returns the {@link RecordingScheduler}.
+ */
+ @Override
+ @Nullable
+ public RecordingScheduler getRecordingScheduler() {
+ return mRecordingScheduler;
+ }
+
+ /**
* Returns the {@link DvrWatchedPositionManager}.
*/
@Override
@@ -268,24 +326,55 @@ 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;
}
+ @Override
+ public boolean isChannelDataManagerLoadFinished() {
+ return mChannelDataManager != null && mChannelDataManager.isDbLoadFinished();
+ }
+
/**
* Returns {@link ProgramDataManager}.
*/
@Override
public ProgramDataManager getProgramDataManager() {
- if (mProgramDataManager == null) {
- mProgramDataManager = new ProgramDataManager(this);
- mProgramDataManager.start();
+ if (mProgramDataManager != null) {
+ return mProgramDataManager;
}
+ Utils.runInMainThreadAndWait(new Runnable() {
+ @Override
+ public void run() {
+ if (mProgramDataManager == null) {
+ mProgramDataManager = new ProgramDataManager(TvApplication.this);
+ mProgramDataManager.start();
+ }
+ }
+ });
return mProgramDataManager;
}
+ @Override
+ public boolean isProgramDataManagerCurrentProgramsLoadFinished() {
+ return mProgramDataManager != null && mProgramDataManager.isCurrentProgramsLoadFinished();
+ }
+
+ /**
+ * Returns {@link PreviewDataManager}.
+ */
+ @TargetApi(Build.VERSION_CODES.O)
+ @Override
+ public PreviewDataManager getPreviewDataManager() {
+ if (mPreviewDataManager == null) {
+ mPreviewDataManager = new PreviewDataManager(this);
+ mPreviewDataManager.start();
+ }
+ return mPreviewDataManager;
+ }
+
/**
* Returns {@link DvrDataManager}.
*/
@@ -314,6 +403,10 @@ public class TvApplication extends Application implements ApplicationSingletons
*/
@Override
public TvInputManagerHelper getTvInputManagerHelper() {
+ if (mTvInputManagerHelper == null) {
+ mTvInputManagerHelper = new TvInputManagerHelper(this);
+ mTvInputManagerHelper.start();
+ }
return mTvInputManagerHelper;
}
@@ -345,6 +438,19 @@ public class TvApplication extends Application implements ApplicationSingletons
return mRemoteConfig;
}
+ @Override
+ public boolean isRunningInMainProcess() {
+ return mRunningInMainProcess != null && mRunningInMainProcess;
+ }
+
+ @Override
+ public PerformanceMonitor getPerformanceMonitor() {
+ if (mPerformanceMonitor == null) {
+ mPerformanceMonitor = StubPerformanceMonitor.initialize(this);
+ }
+ return mPerformanceMonitor;
+ }
+
/**
* SelectInputActivity is set in {@link SelectInputActivity#onCreate} and cleared in
* {@link SelectInputActivity#onDestroy}.
@@ -353,6 +459,14 @@ public class TvApplication extends Application implements ApplicationSingletons
mSelectInputActivity = activity;
}
+ public void handleGuideKey() {
+ if (!mMainActivityWrapper.isResumed()) {
+ startActivity(new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI));
+ } else {
+ mMainActivityWrapper.getMainActivity().getOverlayManager().toggleProgramGuide();
+ }
+ }
+
/**
* Handles the global key KEYCODE_TV.
*/
@@ -471,6 +585,7 @@ public class TvApplication extends Application implements ApplicationSingletons
if (packageManager.getComponentEnabledSetting(name) != newState) {
packageManager.setComponentEnabledSetting(name, newState,
dontKillApp ? PackageManager.DONT_KILL_APP : 0);
+ Log.i(TAG, (enable ? "Un-hide" : "Hide") + " Live TV.");
}
SetupUtils.getInstance(TvApplication.this).onInputListUpdated(inputManager);
}
@@ -479,7 +594,11 @@ public class TvApplication extends Application implements ApplicationSingletons
* Returns the @{@link ApplicationSingletons} using the application context.
*/
public static ApplicationSingletons getSingletons(Context context) {
- return (ApplicationSingletons) context.getApplicationContext();
+ // No need to be "synchronized" because this doesn't create any instance.
+ if (sAppSingletons == null) {
+ sAppSingletons = (ApplicationSingletons) context.getApplicationContext();
+ }
+ return sAppSingletons;
}
/**
@@ -491,6 +610,7 @@ public class TvApplication extends Application implements ApplicationSingletons
* specific initializations.
*/
public static void setCurrentRunningProcess(Context context, boolean isMainProcess) {
+ // TODO(b/63064354) TvApplication should not have to know if it is "the main process"
if (context.getApplicationContext() instanceof TvApplication) {
TvApplication tvApplication = (TvApplication) context.getApplicationContext();
tvApplication.setCurrentRunningProcess(isMainProcess);
diff --git a/src/com/android/tv/TvOptionsManager.java b/src/com/android/tv/TvOptionsManager.java
index 7871cbe7..493e039c 100644
--- a/src/com/android/tv/TvOptionsManager.java
+++ b/src/com/android/tv/TvOptionsManager.java
@@ -18,14 +18,13 @@ package com.android.tv;
import android.content.Context;
import android.media.tv.TvTrackInfo;
+import android.support.annotation.IntDef;
import android.util.SparseArray;
import com.android.tv.data.DisplayMode;
-import com.android.tv.util.TvSettings;
-import com.android.tv.util.TvSettings.PipLayout;
-import com.android.tv.util.TvSettings.PipSize;
-import com.android.tv.util.TvSettings.PipSound;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
/**
@@ -33,39 +32,34 @@ import java.util.Locale;
* captions and display mode. Can be also used to create MenuAction items to control such options.
*/
public class TvOptionsManager {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({OPTION_CLOSED_CAPTIONS, OPTION_DISPLAY_MODE, OPTION_SYSTEMWIDE_PIP, OPTION_MULTI_AUDIO,
+ OPTION_MORE_CHANNELS, OPTION_DEVELOPER, OPTION_SETTINGS})
+ public @interface OptionType {}
public static final int OPTION_CLOSED_CAPTIONS = 0;
public static final int OPTION_DISPLAY_MODE = 1;
- public static final int OPTION_IN_APP_PIP = 2;
- public static final int OPTION_SYSTEMWIDE_PIP = 3;
- public static final int OPTION_MULTI_AUDIO = 4;
- public static final int OPTION_MORE_CHANNELS = 5;
- public static final int OPTION_DEVELOPER = 6;
- public static final int OPTION_SETTINGS = 7;
-
- public static final int OPTION_PIP_INPUT = 100;
- public static final int OPTION_PIP_SWAP = 101;
- public static final int OPTION_PIP_SOUND = 102;
- public static final int OPTION_PIP_LAYOUT = 103 ;
- public static final int OPTION_PIP_SIZE = 104;
+ public static final int OPTION_SYSTEMWIDE_PIP = 2;
+ public static final int OPTION_MULTI_AUDIO = 3;
+ public static final int OPTION_MORE_CHANNELS = 4;
+ public static final int OPTION_DEVELOPER = 5;
+ public static final int OPTION_SETTINGS = 6;
private final Context mContext;
private final SparseArray<OptionChangedListener> mOptionChangedListeners = new SparseArray<>();
private String mClosedCaptionsLanguage;
private int mDisplayMode;
- private boolean mPip;
private String mMultiAudio;
- private String mPipInput;
- private boolean mPipSwap;
- @PipSound private int mPipSound;
- @PipLayout private int mPipLayout;
- @PipSize private int mPipSize;
public TvOptionsManager(Context context) {
mContext = context;
}
- public String getOptionString(int option) {
+ /**
+ * Returns a suitable displayed string for the given option type under current settings.
+ * @param option the type of option, should be one of {@link OptionType}.
+ */
+ public String getOptionString(@OptionType int option) {
switch (option) {
case OPTION_CLOSED_CAPTIONS:
if (mClosedCaptionsLanguage == null) {
@@ -77,101 +71,48 @@ public class TvOptionsManager {
.isDisplayModeAvailable(mDisplayMode)
? DisplayMode.getLabel(mDisplayMode, mContext)
: DisplayMode.getLabel(DisplayMode.MODE_NORMAL, mContext);
- case OPTION_IN_APP_PIP:
- return mContext.getString(
- mPip ? R.string.options_item_pip_on : R.string.options_item_pip_off);
case OPTION_MULTI_AUDIO:
return mMultiAudio;
- case OPTION_PIP_INPUT:
- return mPipInput;
- case OPTION_PIP_SWAP:
- return mContext.getString(mPipSwap ? R.string.pip_options_item_swap_on
- : R.string.pip_options_item_swap_off);
- case OPTION_PIP_SOUND:
- if (mPipSound == TvSettings.PIP_SOUND_MAIN) {
- return mContext.getString(R.string.pip_options_item_sound_main);
- } else if (mPipSound == TvSettings.PIP_SOUND_PIP_WINDOW) {
- return mContext.getString(R.string.pip_options_item_sound_pip_window);
- }
- break;
- case OPTION_PIP_LAYOUT:
- if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_RIGHT) {
- return mContext.getString(R.string.pip_options_item_layout_bottom_right);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_RIGHT) {
- return mContext.getString(R.string.pip_options_item_layout_top_right);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_TOP_LEFT) {
- return mContext.getString(R.string.pip_options_item_layout_top_left);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_BOTTOM_LEFT) {
- return mContext.getString(R.string.pip_options_item_layout_bottom_left);
- } else if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- return mContext.getString(R.string.pip_options_item_layout_side_by_side);
- }
- break;
- case OPTION_PIP_SIZE:
- if (mPipSize == TvSettings.PIP_SIZE_BIG) {
- return mContext.getString(R.string.pip_options_item_size_big);
- } else if (mPipSize == TvSettings.PIP_SIZE_SMALL) {
- return mContext.getString(R.string.pip_options_item_size_small);
- }
- break;
}
return "";
}
- public void onClosedCaptionsChanged(TvTrackInfo track) {
- mClosedCaptionsLanguage = (track == null) ? null
- : (track.getLanguage() != null) ? track.getLanguage()
- : mContext.getString(R.string.default_language);
+ /**
+ * Handles changing selection of closed caption.
+ */
+ public void onClosedCaptionsChanged(TvTrackInfo track, int trackIndex) {
+ mClosedCaptionsLanguage = (track == null) ?
+ null : (track.getLanguage() != null) ? track.getLanguage()
+ : mContext.getString(R.string.closed_caption_unknown_language, trackIndex + 1);
notifyOptionChanged(OPTION_CLOSED_CAPTIONS);
}
+ /**
+ * Handles changing selection of display mode.
+ */
public void onDisplayModeChanged(int displayMode) {
mDisplayMode = displayMode;
notifyOptionChanged(OPTION_DISPLAY_MODE);
}
- public void onPipChanged(boolean pip) {
- mPip = pip;
- notifyOptionChanged(OPTION_IN_APP_PIP);
- }
-
+ /**
+ * Handles changing selection of multi-audio.
+ */
public void onMultiAudioChanged(String multiAudio) {
mMultiAudio = multiAudio;
notifyOptionChanged(OPTION_MULTI_AUDIO);
}
- public void onPipInputChanged(String pipInput) {
- mPipInput = pipInput;
- notifyOptionChanged(OPTION_PIP_INPUT);
- }
-
- public void onPipSwapChanged(boolean pipSwap) {
- mPipSwap = pipSwap;
- notifyOptionChanged(OPTION_PIP_SWAP);
- }
-
- public void onPipSoundChanged(@PipSound int pipSound) {
- mPipSound = pipSound;
- notifyOptionChanged(OPTION_PIP_SOUND);
- }
-
- public void onPipLayoutChanged(@PipLayout int pipLayout) {
- mPipLayout = pipLayout;
- notifyOptionChanged(OPTION_PIP_LAYOUT);
- }
-
- public void onPipSizeChanged(@PipSize int pipSize) {
- mPipSize = pipSize;
- notifyOptionChanged(OPTION_PIP_SIZE);
- }
-
- private void notifyOptionChanged(int option) {
+ private void notifyOptionChanged(@OptionType int option) {
OptionChangedListener listener = mOptionChangedListeners.get(option);
if (listener != null) {
- listener.onOptionChanged(getOptionString(option));
+ listener.onOptionChanged(option, getOptionString(option));
}
}
+ /**
+ * Sets listeners to changes of the given option type.
+ */
public void setOptionChangedListener(int option, OptionChangedListener listener) {
mOptionChangedListeners.put(option, listener);
}
@@ -180,6 +121,6 @@ public class TvOptionsManager {
* An interface used to monitor option changes.
*/
public interface OptionChangedListener {
- void onOptionChanged(String newOption);
+ void onOptionChanged(@OptionType int optionType, String newString);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/config/DefaultConfigManager.java b/src/com/android/tv/config/DefaultConfigManager.java
index f5a6e959..bbabc6d4 100644
--- a/src/com/android/tv/config/DefaultConfigManager.java
+++ b/src/com/android/tv/config/DefaultConfigManager.java
@@ -22,6 +22,7 @@ import android.content.Context;
* Stub Remote Config.
*/
public class DefaultConfigManager {
+ public static final long DEFAULT_LONG_VALUE = 0;
public static DefaultConfigManager createInstance(Context context) {
return new DefaultConfigManager();
}
@@ -47,6 +48,11 @@ public class DefaultConfigManager {
public boolean getBoolean(String key) {
return false;
}
+
+ @Override
+ public long getLong(String key) {
+ return DEFAULT_LONG_VALUE;
+ }
}
}
diff --git a/src/com/android/tv/config/RemoteConfig.java b/src/com/android/tv/config/RemoteConfig.java
index 0f7d2c53..f7ae87e7 100644
--- a/src/com/android/tv/config/RemoteConfig.java
+++ b/src/com/android/tv/config/RemoteConfig.java
@@ -45,4 +45,7 @@ public interface RemoteConfig {
* Gets value as a boolean corresponding to the specified key.
*/
boolean getBoolean(String key);
+
+ /** Gets value as a long corresponding to the specified key. */
+ long getLong(String key);
}
diff --git a/src/com/android/tv/config/RemoteConfigUtils.java b/src/com/android/tv/config/RemoteConfigUtils.java
new file mode 100644
index 00000000..09d85239
--- /dev/null
+++ b/src/com/android/tv/config/RemoteConfigUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.config;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.tv.TvApplication;
+
+/** A utility class to get the remote config. */
+public class RemoteConfigUtils {
+ private static final String TAG = "RemoteConfigUtils";
+ private static final boolean DEBUG = false;
+
+ private RemoteConfigUtils() {}
+
+ public static long getRemoteConfig(Context context, String key, long defaultValue) {
+ RemoteConfig remoteConfig = TvApplication.getSingletons(context).getRemoteConfig();
+ try {
+ long remoteValue = remoteConfig.getLong(key);
+ if (DEBUG) Log.d(TAG, "Got " + key + " from remote: " + remoteValue);
+ return remoteValue;
+ } catch (Exception e) {
+ Log.w(TAG, "Cannot get " + key + " from RemoteConfig", e);
+ }
+ if (DEBUG) Log.d(TAG, "Use default value " + defaultValue);
+ return defaultValue;
+ }
+}
diff --git a/src/com/android/tv/customization/TvCustomizationManager.java b/src/com/android/tv/customization/TvCustomizationManager.java
index 22298a10..ed6b98ca 100644
--- a/src/com/android/tv/customization/TvCustomizationManager.java
+++ b/src/com/android/tv/customization/TvCustomizationManager.java
@@ -18,15 +18,18 @@ package com.android.tv.customization;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
import android.text.TextUtils;
import android.util.Log;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -37,6 +40,10 @@ public class TvCustomizationManager {
private static final String TAG = "TvCustomizationManager";
private static final boolean DEBUG = false;
+ private static final String[] CUSTOMIZE_PERMISSIONS = {
+ "com.android.tv.permission.CUSTOMIZE_TV_APP"
+ };
+
private static final String CATEGORY_TV_CUSTOMIZATION =
"com.android.tv.category";
@@ -47,6 +54,19 @@ public class TvCustomizationManager {
public static final String ID_OPTIONS_ROW = "options_row";
public static final String ID_PARTNER_ROW = "partner_row";
+ @IntDef({TRICKPLAY_MODE_ENABLED, TRICKPLAY_MODE_DISABLED, TRICKPLAY_MODE_USE_EXTERNAL_STORAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TRICKPLAY_MODE {}
+ public static final int TRICKPLAY_MODE_ENABLED = 0;
+ public static final int TRICKPLAY_MODE_DISABLED = 1;
+ public static final int TRICKPLAY_MODE_USE_EXTERNAL_STORAGE = 2;
+
+ private static final String[] TRICKPLAY_MODE_STRINGS = {
+ "enabled",
+ "disabled",
+ "use_external_storage_only"
+ };
+
private static final HashMap<String, String> INTENT_CATEGORY_TO_ROW_ID;
static {
INTENT_CATEGORY_TO_ROW_ID = new HashMap<>();
@@ -55,12 +75,19 @@ public class TvCustomizationManager {
}
private static final String RES_ID_PARTNER_ROW_TITLE = "partner_row_title";
+ private static final String RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER =
+ "has_linux_dvb_built_in_tuner";
+ private static final String RES_ID_TRICKPLAY_MODE = "trickplay_mode";
private static final String RES_TYPE_STRING = "string";
+ private static final String RES_TYPE_BOOLEAN = "bool";
+
+ private static String sCustomizationPackage;
+ private static Boolean sHasLinuxDvbBuiltInTuner;
+ private static @TRICKPLAY_MODE Integer sTrickplayMode;
private final Context mContext;
private boolean mInitialized;
- private String mCustomizationPackage;
private String mPartnerRowTitle;
private final Map<String, List<CustomAction>> mRowIdToCustomActionsMap = new HashMap<>();
@@ -71,6 +98,68 @@ public class TvCustomizationManager {
}
/**
+ * Returns {@code true} if there's a customization package installed and it specifies built-in
+ * tuner devices are available. The built-in tuner should support DVB API to be recognized by
+ * Live TV.
+ */
+ public static boolean hasLinuxDvbBuiltInTuner(Context context) {
+ if (sHasLinuxDvbBuiltInTuner == null) {
+ if (TextUtils.isEmpty(getCustomizationPackageName(context))) {
+ sHasLinuxDvbBuiltInTuner = false;
+ } else {
+ try {
+ Resources res = context.getPackageManager()
+ .getResourcesForApplication(sCustomizationPackage);
+ int resId = res.getIdentifier(RES_ID_HAS_LINUX_DVB_BUILT_IN_TUNER,
+ RES_TYPE_BOOLEAN, sCustomizationPackage);
+ sHasLinuxDvbBuiltInTuner = resId != 0 && res.getBoolean(resId);
+ } catch (NameNotFoundException e) {
+ sHasLinuxDvbBuiltInTuner = false;
+ }
+ }
+ }
+ return sHasLinuxDvbBuiltInTuner;
+ }
+
+ public static @TRICKPLAY_MODE int getTrickplayMode(Context context) {
+ if (sTrickplayMode == null) {
+ if (TextUtils.isEmpty(getCustomizationPackageName(context))) {
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ } else {
+ try {
+ String customization = null;
+ Resources res = context.getPackageManager()
+ .getResourcesForApplication(sCustomizationPackage);
+ int resId = res.getIdentifier(RES_ID_TRICKPLAY_MODE,
+ RES_TYPE_STRING, sCustomizationPackage);
+ customization = resId == 0 ? null : res.getString(resId);
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ if (customization != null) {
+ for (int i = 0; i < TRICKPLAY_MODE_STRINGS.length; ++i) {
+ if (TRICKPLAY_MODE_STRINGS[i].equalsIgnoreCase(customization)) {
+ sTrickplayMode = i;
+ break;
+ }
+ }
+ }
+ } catch (NameNotFoundException e) {
+ sTrickplayMode = TRICKPLAY_MODE_ENABLED;
+ }
+ }
+ }
+ return sTrickplayMode;
+ }
+
+ private static String getCustomizationPackageName(Context context) {
+ if (sCustomizationPackage == null) {
+ List<PackageInfo> packageInfos = context.getPackageManager()
+ .getPackagesHoldingPermissions(CUSTOMIZE_PERMISSIONS, 0);
+ sCustomizationPackage = packageInfos.size() == 0 ? "" : packageInfos.get(0).packageName;
+ }
+ return sCustomizationPackage;
+ }
+
+ /**
* Initialize TV customization options.
* Run this API only on the main thread.
*/
@@ -79,14 +168,13 @@ public class TvCustomizationManager {
return;
}
mInitialized = true;
- buildCustomActions();
- if (!TextUtils.isEmpty(mCustomizationPackage)) {
+ if (!TextUtils.isEmpty(getCustomizationPackageName(mContext))) {
+ buildCustomActions();
buildPartnerRow();
}
}
private void buildCustomActions() {
- mCustomizationPackage = null;
mRowIdToCustomActionsMap.clear();
PackageManager pm = mContext.getPackageManager();
for (String intentCategory : INTENT_CATEGORY_TO_ROW_ID.keySet()) {
@@ -98,16 +186,8 @@ public class TvCustomizationManager {
| PackageManager.GET_META_DATA);
for (ResolveInfo info : activities) {
String packageName = info.activityInfo.packageName;
- if (TextUtils.isEmpty(mCustomizationPackage)) {
- if (DEBUG) Log.d(TAG, "Found TV customization package " + packageName);
- if ((info.activityInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_SYSTEM) == 0) {
- Log.w(TAG, "Only system app can customize TV. Ignoring " + packageName);
- continue;
- }
- mCustomizationPackage = packageName;
- } else if (!packageName.equals(mCustomizationPackage)) {
- Log.w(TAG, "A customization package " + mCustomizationPackage
+ if (!TextUtils.equals(packageName, sCustomizationPackage)) {
+ Log.w(TAG, "A customization package " + sCustomizationPackage
+ " already exist. Ignoring " + packageName);
continue;
}
@@ -117,7 +197,7 @@ public class TvCustomizationManager {
Drawable drawable = info.loadIcon(pm);
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(intentCategory);
- intent.setClassName(mCustomizationPackage, info.activityInfo.name);
+ intent.setClassName(sCustomizationPackage, info.activityInfo.name);
String rowId = INTENT_CATEGORY_TO_ROW_ID.get(intentCategory);
List<CustomAction> actions = mRowIdToCustomActionsMap.get(rowId);
@@ -159,13 +239,13 @@ public class TvCustomizationManager {
Resources res;
try {
res = mContext.getPackageManager()
- .getResourcesForApplication(mCustomizationPackage);
+ .getResourcesForApplication(sCustomizationPackage);
} catch (NameNotFoundException e) {
- Log.w(TAG, "Could not get resources for package " + mCustomizationPackage);
+ Log.w(TAG, "Could not get resources for package " + sCustomizationPackage);
return;
}
int resId = res.getIdentifier(
- RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, mCustomizationPackage);
+ RES_ID_PARTNER_ROW_TITLE, RES_TYPE_STRING, sCustomizationPackage);
if (resId != 0) {
mPartnerRowTitle = res.getString(resId);
}
diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java
index f420de02..4e36c80a 100644
--- a/src/com/android/tv/data/BaseProgram.java
+++ b/src/com/android/tv/data/BaseProgram.java
@@ -17,12 +17,17 @@
package com.android.tv.data;
import android.content.Context;
+import android.media.tv.TvContentRating;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.R;
import java.util.Comparator;
/**
* Base class for {@link com.android.tv.data.Program} and
- * {@link com.android.tv.dvr.RecordedProgram}.
+ * {@link com.android.tv.dvr.data.RecordedProgram}.
*/
public abstract class BaseProgram {
/**
@@ -94,14 +99,29 @@ public abstract class BaseProgram {
abstract public String getTitle();
/**
- * Returns the program's title withe its season and episode number.
+ * Returns the episode title.
*/
- abstract public String getTitleWithEpisodeNumber(Context context);
+ abstract public String getEpisodeTitle();
/**
* Returns the displayed title of the program episode.
*/
- abstract public String getEpisodeDisplayTitle(Context context);
+ public String getEpisodeDisplayTitle(Context context) {
+ if (!TextUtils.isEmpty(getEpisodeNumber())) {
+ String episodeTitle = getEpisodeTitle() == null ? "" : getEpisodeTitle();
+ if (TextUtils.equals(getSeasonNumber(), "0")) {
+ // Do not show "S0: ".
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format_no_season_number),
+ getEpisodeNumber(), episodeTitle);
+ } else {
+ return String.format(context.getResources().getString(
+ R.string.display_episode_title_format),
+ getSeasonNumber(), getEpisodeNumber(), episodeTitle);
+ }
+ }
+ return getEpisodeTitle();
+ }
/**
* Returns the description of the program.
@@ -158,6 +178,10 @@ public abstract class BaseProgram {
*/
abstract public int[] getCanonicalGenreIds();
+ /** Returns the array of content ratings. */
+ @Nullable
+ abstract public TvContentRating[] getContentRatings();
+
/**
* Returns channel's ID of the program.
*/
@@ -169,6 +193,13 @@ public abstract class BaseProgram {
abstract public boolean isValid();
/**
+ * Checks whether the program is episodic or not.
+ */
+ public boolean isEpisodic() {
+ return getSeriesId() != null;
+ }
+
+ /**
* Generates the series ID for the other inputs than the tuner TV input.
*/
public static String generateSeriesId(String packageName, String title) {
diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java
index 30f84236..4a391ae7 100644
--- a/src/com/android/tv/data/Channel.java
+++ b/src/com/android/tv/data/Channel.java
@@ -52,6 +52,16 @@ public final class Channel {
public static final int LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART = 3;
/**
+ * Compares the channel numbers of channels which belong to the same input.
+ */
+ public static final Comparator<Channel> CHANNEL_NUMBER_COMPARATOR = new Comparator<Channel>() {
+ @Override
+ public int compare(Channel lhs, Channel rhs) {
+ return ChannelNumber.compare(lhs.getDisplayNumber(), rhs.getDisplayNumber());
+ }
+ };
+
+ /**
* When a TIS doesn't provide any information about app link, and it doesn't have a leanback
* launch intent, there will be no app link card for the TIS.
*/
@@ -81,15 +91,22 @@ public final class Channel {
TvContract.Channels.COLUMN_DESCRIPTION,
TvContract.Channels.COLUMN_VIDEO_FORMAT,
TvContract.Channels.COLUMN_BROWSABLE,
+ TvContract.Channels.COLUMN_SEARCHABLE,
TvContract.Channels.COLUMN_LOCKED,
TvContract.Channels.COLUMN_APP_LINK_TEXT,
TvContract.Channels.COLUMN_APP_LINK_COLOR,
TvContract.Channels.COLUMN_APP_LINK_ICON_URI,
TvContract.Channels.COLUMN_APP_LINK_POSTER_ART_URI,
TvContract.Channels.COLUMN_APP_LINK_INTENT_URI,
+ TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, // Only used in bundled input
};
/**
+ * Channel number delimiter between major and minor parts.
+ */
+ public static final char CHANNEL_NUMBER_DELIMITER = '-';
+
+ /**
* Creates {@code Channel} object from cursor.
*
* <p>The query that created the cursor MUST use {@link #PROJECTION}
@@ -103,28 +120,41 @@ 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++));
channel.mBrowsable = cursor.getInt(index++) == 1;
+ channel.mSearchable = cursor.getInt(index++) == 1;
channel.mLocked = cursor.getInt(index++) == 1;
channel.mAppLinkText = cursor.getString(index++);
channel.mAppLinkColor = cursor.getInt(index++);
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. */
@@ -138,6 +168,7 @@ public final class Channel {
private String mDescription;
private String mVideoFormat;
private boolean mBrowsable;
+ private boolean mSearchable;
private boolean mLocked;
private boolean mIsPassthrough;
private String mAppLinkText;
@@ -147,8 +178,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.
@@ -187,7 +220,6 @@ public final class Channel {
return mDisplayName;
}
- @VisibleForTesting
public String getDescription() {
return mDescription;
}
@@ -230,10 +262,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;
}
/**
@@ -266,6 +302,11 @@ public final class Channel {
return mBrowsable;
}
+ /** Checks whether this channel is searchable or not. */
+ public boolean isSearchable() {
+ return mSearchable;
+ }
+
public boolean isLocked() {
return mLocked;
}
@@ -279,6 +320,13 @@ public final class Channel {
}
/**
+ * Sets channel logo uri which is got from cloud.
+ */
+ public void setLogoUri(String logoUri) {
+ mLogoUri = logoUri;
+ }
+
+ /**
* Check whether {@code other} has same read-only channel info as this. But, it cannot check two
* channels have same logos. It also excludes browsable and locked, because two fields are
* changed by TV app.
@@ -298,7 +346,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
@@ -314,8 +363,10 @@ public final class Channel {
+ ", videoFormat=" + mVideoFormat
+ ", isPassthrough=" + mIsPassthrough
+ ", browsable=" + mBrowsable
+ + ", searchable=" + mSearchable
+ ", locked=" + mLocked
- + ", appLinkText=" + mAppLinkText + "}";
+ + ", appLinkText=" + mAppLinkText
+ + ", recordingProhibited=" + mRecordingProhibited + "}";
}
void copyFrom(Channel other) {
@@ -332,6 +383,7 @@ public final class Channel {
mVideoFormat = other.mVideoFormat;
mIsPassthrough = other.mIsPassthrough;
mBrowsable = other.mBrowsable;
+ mSearchable = other.mSearchable;
mLocked = other.mLocked;
mAppLinkText = other.mAppLinkText;
mAppLinkColor = other.mAppLinkColor;
@@ -340,6 +392,8 @@ public final class Channel {
mAppLinkIntentUri = other.mAppLinkIntentUri;
mAppLinkIntent = other.mAppLinkIntent;
mAppLinkType = other.mAppLinkType;
+ mRecordingProhibited = other.mRecordingProhibited;
+ mChannelLogoExist = other.mChannelLogoExist;
}
/**
@@ -389,8 +443,7 @@ public final class Channel {
mChannel.mDisplayName = "name";
mChannel.mDescription = "description";
mChannel.mBrowsable = true;
- mChannel.mLocked = false;
- mChannel.mIsPassthrough = false;
+ mChannel.mSearchable = true;
}
public Builder(Channel other) {
@@ -422,7 +475,7 @@ public final class Channel {
@VisibleForTesting
public Builder setDisplayNumber(String displayNumber) {
- mChannel.mDisplayNumber = displayNumber;
+ mChannel.mDisplayNumber = normalizeDisplayNumber(displayNumber);
return this;
}
@@ -448,6 +501,11 @@ public final class Channel {
return this;
}
+ public Builder setSearchable(boolean searchable) {
+ mChannel.mSearchable = searchable;
+ return this;
+ }
+
public Builder setLocked(boolean locked) {
mChannel.mLocked = locked;
return this;
@@ -485,6 +543,11 @@ public final class Channel {
return this;
}
+ public Builder setRecordingProhibited(boolean recordingProhibited) {
+ mChannel.mRecordingProhibited = recordingProhibited;
+ return this;
+ }
+
public Channel build() {
Channel channel = new Channel();
channel.copyFrom(mChannel);
@@ -524,6 +587,21 @@ public final class Channel {
}
/**
+ * Sets if the channel logo exists. This method should be only called from
+ * {@link ChannelDataManager}.
+ */
+ void setChannelLogoExist(boolean exist) {
+ mChannelLogoExist = exist;
+ }
+
+ /**
+ * Returns if channel logo exists.
+ */
+ public boolean channelLogoExists() {
+ return mChannelLogoExist;
+ }
+
+ /**
* Returns the type of app link for this channel.
* It returns {@link #APP_LINK_TYPE_CHANNEL} if the channel has a non null app link text and
* a valid app link intent, it returns {@link #APP_LINK_TYPE_APP} if the input service which
@@ -655,4 +733,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..6f93fbd1 100644
--- a/src/com/android/tv/data/ChannelDataManager.java
+++ b/src/com/android/tv/data/ChannelDataManager.java
@@ -21,13 +21,17 @@ 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.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
@@ -43,6 +47,7 @@ import com.android.tv.util.PermissionUtils;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -59,7 +64,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
* This class is not thread-safe and under an assumption that its public methods are called in
* only the main thread.
*/
-@MainThread
+@AnyThread
public class ChannelDataManager {
private static final String TAG = "ChannelDataManager";
private static final boolean DEBUG = false;
@@ -74,10 +79,10 @@ public class ChannelDataManager {
private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
private final Set<Listener> mListeners = new CopyOnWriteArraySet<>();
- private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
- private final Map<String, MutableInt> mChannelCountMap = new HashMap<>();
+ // Use container class to support multi-thread safety. This value can be set only on the main
+ // thread.
+ volatile private UnmodifiableChannelData mData = new UnmodifiableChannelData();
private final Channel.DefaultComparator mChannelComparator;
- private final List<Channel> mChannels = new ArrayList<>();
private final Handler mHandler;
private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>();
@@ -92,15 +97,17 @@ public class ChannelDataManager {
@Override
public void onInputAdded(String inputId) {
boolean channelAdded = false;
- for (ChannelWrapper channel : mChannelWrapperMap.values()) {
+ ChannelData data = new ChannelData(mData);
+ for (ChannelWrapper channel : mData.channelWrapperMap.values()) {
if (channel.mChannel.getInputId().equals(inputId)) {
channel.mInputRemoved = false;
- addChannel(channel.mChannel);
+ addChannel(data, channel.mChannel);
channelAdded = true;
}
}
if (channelAdded) {
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
notifyChannelListUpdated();
}
}
@@ -109,7 +116,7 @@ public class ChannelDataManager {
public void onInputRemoved(String inputId) {
boolean channelRemoved = false;
ArrayList<ChannelWrapper> removedChannels = new ArrayList<>();
- for (ChannelWrapper channel : mChannelWrapperMap.values()) {
+ for (ChannelWrapper channel : mData.channelWrapperMap.values()) {
if (channel.mChannel.getInputId().equals(inputId)) {
channel.mInputRemoved = true;
channelRemoved = true;
@@ -117,13 +124,15 @@ public class ChannelDataManager {
}
}
if (channelRemoved) {
- clearChannels();
- for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
+ ChannelData data = new ChannelData();
+ data.channelWrapperMap.putAll(mData.channelWrapperMap);
+ for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) {
if (!channelWrapper.mInputRemoved) {
- addChannel(channelWrapper.mChannel);
+ addChannel(data, channelWrapper.mChannel);
}
}
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
notifyChannelListUpdated();
for (ChannelWrapper channel : removedChannels) {
channel.notifyChannelRemoved();
@@ -132,10 +141,12 @@ public class ChannelDataManager {
}
};
+ @MainThread
public ChannelDataManager(Context context, TvInputManagerHelper inputManager) {
this(context, inputManager, context.getContentResolver());
}
+ @MainThread
@VisibleForTesting
ChannelDataManager(Context context, TvInputManagerHelper inputManager,
ContentResolver contentResolver) {
@@ -167,6 +178,7 @@ public class ChannelDataManager {
/**
* Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called.
*/
+ @MainThread
public void start() {
if (mStarted) {
return;
@@ -184,6 +196,7 @@ public class ChannelDataManager {
* Stops the manager. It clears manager states and runs pending DB operations. Added listeners
* aren't automatically removed by this method.
*/
+ @MainThread
@VisibleForTesting
public void stop() {
if (!mStarted) {
@@ -192,12 +205,10 @@ public class ChannelDataManager {
mStarted = false;
mDbLoadFinished = false;
- ChannelLogoFetcher.stopFetchingChannelLogos();
mInputManager.removeCallback(mTvInputCallback);
mContentResolver.unregisterContentObserver(mChannelObserver);
mHandler.removeCallbacksAndMessages(null);
- mChannelWrapperMap.clear();
clearChannels();
mPostRunnablesAfterChannelUpdate.clear();
if (mChannelsUpdateTask != null) {
@@ -233,7 +244,7 @@ public class ChannelDataManager {
* Adds a {@link ChannelListener} for a specific channel with the channel ID {@code channelId}.
*/
public void addChannelListener(Long channelId, ChannelListener listener) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -245,7 +256,7 @@ public class ChannelDataManager {
* {@code channelId}.
*/
public void removeChannelListener(Long channelId, ChannelListener listener) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -263,14 +274,14 @@ public class ChannelDataManager {
* Returns the number of channels.
*/
public int getChannelCount() {
- return mChannels.size();
+ return mData.channels.size();
}
/**
* Returns a list of channels.
*/
public List<Channel> getChannelList() {
- return Collections.unmodifiableList(mChannels);
+ return new ArrayList<>(mData.channels);
}
/**
@@ -278,7 +289,7 @@ public class ChannelDataManager {
*/
public List<Channel> getBrowsableChannelList() {
List<Channel> channels = new ArrayList<>();
- for (Channel channel : mChannels) {
+ for (Channel channel : mData.channels) {
if (channel.isBrowsable()) {
channels.add(channel);
}
@@ -292,7 +303,7 @@ public class ChannelDataManager {
* @param inputId The ID of the input.
*/
public int getChannelCountForInput(String inputId) {
- MutableInt count = mChannelCountMap.get(inputId);
+ MutableInt count = mData.channelCountMap.get(inputId);
return count == null ? 0 : count.value;
}
@@ -303,17 +314,14 @@ public class ChannelDataManager {
* In that case this method is used to check if the channel exists in the DB.
*/
public boolean doesChannelExistInDb(long channelId) {
- return mChannelWrapperMap.get(channelId) != null;
+ return mData.channelWrapperMap.get(channelId) != null;
}
/**
* Returns true if and only if there exists at least one channel and all channels are hidden.
*/
public boolean areAllChannelsHidden() {
- if (mChannels.isEmpty()) {
- return false;
- }
- for (Channel channel : mChannels) {
+ for (Channel channel : mData.channels) {
if (channel.isBrowsable()) {
return false;
}
@@ -325,7 +333,7 @@ public class ChannelDataManager {
* Gets the channel with the channel ID {@code channelId}.
*/
public Channel getChannel(Long channelId) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null || channelWrapper.mInputRemoved) {
return null;
}
@@ -349,7 +357,7 @@ public class ChannelDataManager {
*/
public void updateBrowsable(Long channelId, boolean browsable,
boolean skipNotifyChannelBrowsableChanged) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -407,7 +415,7 @@ public class ChannelDataManager {
* The value change will be applied to DB when applyPendingDbOperation is called.
*/
public void updateLocked(Long channelId, boolean locked) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
+ ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
if (channelWrapper == null) {
return;
}
@@ -427,10 +435,11 @@ public class ChannelDataManager {
* to DB.
*/
public void applyUpdatedValuesToDb() {
+ ChannelData data = mData;
ArrayList<Long> browsableIds = new ArrayList<>();
ArrayList<Long> unbrowsableIds = new ArrayList<>();
for (Long id : mBrowsableUpdateChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
if (channelWrapper == null) {
continue;
}
@@ -452,10 +461,10 @@ public class ChannelDataManager {
}
editor.apply();
} else {
- if (browsableIds.size() != 0) {
+ if (!browsableIds.isEmpty()) {
updateOneColumnValue(column, 1, browsableIds);
}
- if (unbrowsableIds.size() != 0) {
+ if (!unbrowsableIds.isEmpty()) {
updateOneColumnValue(column, 0, unbrowsableIds);
}
}
@@ -464,7 +473,7 @@ public class ChannelDataManager {
ArrayList<Long> lockedIds = new ArrayList<>();
ArrayList<Long> unlockedIds = new ArrayList<>();
for (Long id : mLockedUpdateChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
if (channelWrapper == null) {
continue;
}
@@ -476,10 +485,10 @@ public class ChannelDataManager {
channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked();
}
column = TvContract.Channels.COLUMN_LOCKED;
- if (lockedIds.size() != 0) {
+ if (!lockedIds.isEmpty()) {
updateOneColumnValue(column, 1, lockedIds);
}
- if (unlockedIds.size() != 0) {
+ if (!unlockedIds.isEmpty()) {
updateOneColumnValue(column, 0, unlockedIds);
}
mLockedUpdateChannelIds.clear();
@@ -492,22 +501,24 @@ public class ChannelDataManager {
}
}
- private void addChannel(Channel channel) {
- mChannels.add(channel);
+ @MainThread
+ private void addChannel(ChannelData data, Channel channel) {
+ data.channels.add(channel);
String inputId = channel.getInputId();
- MutableInt count = mChannelCountMap.get(inputId);
+ MutableInt count = data.channelCountMap.get(inputId);
if (count == null) {
- mChannelCountMap.put(inputId, new MutableInt(1));
+ data.channelCountMap.put(inputId, new MutableInt(1));
} else {
count.value++;
}
}
+ @MainThread
private void clearChannels() {
- mChannels.clear();
- mChannelCountMap.clear();
+ mData = new UnmodifiableChannelData();
}
+ @MainThread
private void handleUpdateChannels() {
if (mChannelsUpdateTask != null) {
mChannelsUpdateTask.cancel(true);
@@ -525,6 +536,9 @@ public class ChannelDataManager {
}
}
+ /**
+ * A listener for ChannelDataManager. The callbacks are called on the main thread.
+ */
public interface Listener {
/**
* Called when data load is finished.
@@ -543,6 +557,9 @@ public class ChannelDataManager {
void onChannelBrowsableChanged();
}
+ /**
+ * A listener for individual channel change. The callbacks are called on the main thread.
+ */
public interface ChannelListener {
/**
* Called when the channel has been removed in DB.
@@ -590,9 +607,36 @@ public class ChannelDataManager {
}
}
+ private class CheckChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> {
+ private final Channel mChannel;
+
+ CheckChannelLogoExistTask(Channel channel) {
+ mChannel = channel;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try (AssetFileDescriptor f = mContext.getContentResolver().openAssetFileDescriptor(
+ TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
+ return true;
+ } catch (SQLiteException | IOException | NullPointerException e) {
+ // File not found or asset file not found.
+ }
+ return false;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ ChannelWrapper wrapper = mData.channelWrapperMap.get(mChannel.getId());
+ if (wrapper != null) {
+ wrapper.mChannel.setChannelLogoExist(result);
+ }
+ }
+ }
+
private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
- public QueryAllChannelsTask(ContentResolver contentResolver) {
+ QueryAllChannelsTask(ContentResolver contentResolver) {
super(contentResolver);
}
@@ -603,7 +647,9 @@ public class ChannelDataManager {
if (DEBUG) Log.e(TAG, "onPostExecute with null channels");
return;
}
- Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet());
+ ChannelData data = new ChannelData();
+ data.channelWrapperMap.putAll(mData.channelWrapperMap);
+ Set<Long> removedChannelIds = new HashSet<>(data.channelWrapperMap.keySet());
List<ChannelWrapper> removedChannelWrappers = new ArrayList<>();
List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>();
@@ -625,13 +671,15 @@ 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);
+ data.channelWrapperMap.put(channel.getId(), channelWrapper);
if (!channelWrapper.mInputRemoved) {
channelAdded = true;
}
} else {
- channelWrapper = mChannelWrapperMap.get(channelId);
+ channelWrapper = data.channelWrapperMap.get(channelId);
if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) {
// Channel data updated
Channel oldChannel = channelWrapper.mChannel;
@@ -640,9 +688,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);
@@ -663,19 +711,19 @@ public class ChannelDataManager {
}
for (long id : removedChannelIds) {
- ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id);
+ ChannelWrapper channelWrapper = data.channelWrapperMap.remove(id);
if (!channelWrapper.mInputRemoved) {
channelRemoved = true;
removedChannelWrappers.add(channelWrapper);
}
}
- clearChannels();
- for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
+ for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) {
if (!channelWrapper.mInputRemoved) {
- addChannel(channelWrapper.mChannel);
+ addChannel(data, channelWrapper.mChannel);
}
}
- Collections.sort(mChannels, mChannelComparator);
+ Collections.sort(data.channels, mChannelComparator);
+ mData = new UnmodifiableChannelData(data);
if (!mDbLoadFinished) {
mDbLoadFinished = true;
@@ -693,7 +741,6 @@ public class ChannelDataManager {
r.run();
}
mPostRunnablesAfterChannelUpdate.clear();
- ChannelLogoFetcher.startFetchingChannelLogos(mContext);
}
}
@@ -705,10 +752,9 @@ public class ChannelDataManager {
private void updateOneColumnValue(
final String columnName, final int columnValue, final List<Long> ids) {
if (!PermissionUtils.hasAccessAllEpg(mContext)) {
- // TODO: support this feature for non-system LC app. b/23939816
return;
}
- AsyncDbTask.execute(new Runnable() {
+ AsyncDbTask.executeOnDbThread(new Runnable() {
@Override
public void run() {
String selection = Utils.buildSelectionForIds(Channels._ID, ids);
@@ -723,6 +769,7 @@ public class ChannelDataManager {
return channel.getInputId() + "|" + channel.getId();
}
+ @MainThread
private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
super(Looper.getMainLooper(), channelDataManager);
@@ -735,4 +782,51 @@ public class ChannelDataManager {
}
}
}
+
+ /**
+ * Container class which includes channel data that needs to be synced. This class is
+ * modifiable and used for changing channel data.
+ * e.g. TvInputCallback, or AsyncDbTask.onPostExecute.
+ */
+ @MainThread
+ private static class ChannelData {
+ final Map<Long, ChannelWrapper> channelWrapperMap;
+ final Map<String, MutableInt> channelCountMap;
+ final List<Channel> channels;
+
+ ChannelData() {
+ channelWrapperMap = new HashMap<>();
+ channelCountMap = new HashMap<>();
+ channels = new ArrayList<>();
+ }
+
+ ChannelData(ChannelData data) {
+ channelWrapperMap = new HashMap<>(data.channelWrapperMap);
+ channelCountMap = new HashMap<>(data.channelCountMap);
+ channels = new ArrayList<>(data.channels);
+ }
+
+ ChannelData(Map<Long, ChannelWrapper> channelWrapperMap,
+ Map<String, MutableInt> channelCountMap, List<Channel> channels) {
+ this.channelWrapperMap = channelWrapperMap;
+ this.channelCountMap = channelCountMap;
+ this.channels = channels;
+ }
+ }
+
+ /** Unmodifiable channel data. */
+ @MainThread
+ private static class UnmodifiableChannelData extends ChannelData {
+ UnmodifiableChannelData() {
+ super(Collections.unmodifiableMap(new HashMap<>()),
+ Collections.unmodifiableMap(new HashMap<>()),
+ Collections.unmodifiableList(new ArrayList<>()));
+ }
+
+ UnmodifiableChannelData(ChannelData data) {
+ super(Collections.unmodifiableMap(data.channelWrapperMap),
+ Collections.unmodifiableMap(data.channelCountMap),
+ Collections.unmodifiableList(data.channels));
+ }
+ }
}
diff --git a/src/com/android/tv/data/ChannelLogoFetcher.java b/src/com/android/tv/data/ChannelLogoFetcher.java
index 5a549f83..132cab7a 100644
--- a/src/com/android/tv/data/ChannelLogoFetcher.java
+++ b/src/com/android/tv/data/ChannelLogoFetcher.java
@@ -16,160 +16,74 @@
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.MainThread;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.util.AsyncDbTask;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.util.BitmapUtils;
import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
import com.android.tv.util.PermissionUtils;
-import java.io.BufferedReader;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.Set;
+import java.util.List;
/**
- * Utility class for TMS data.
- * This class is thread safe.
+ * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
+ * or need update logos. This class is thread safe.
*/
public class ChannelLogoFetcher {
private static final String TAG = "ChannelLogoFetcher";
private static final boolean DEBUG = false;
- /**
- * The name of the file which contains the TMS data.
- * The file has multiple records and each of them is a string separated by '|' like
- * STATION_NAME|SHORT_NAME|CALL_SIGN|LOGO_URI.
- */
- private static final String TMS_US_TABLE_FILE = "tms_us.table";
- private static final String TMS_KR_TABLE_FILE = "tms_kr.table";
- private static final String FIELD_SEPARATOR = "\\|";
- private static final String NAME_SEPARATOR_FOR_TMS = "\\(|\\)|\\{|\\}|\\[|\\]";
- private static final String NAME_SEPARATOR_FOR_DB = "\\W";
- private static final int INDEX_NAME = 0;
- private static final int INDEX_SHORT_NAME = 1;
- private static final int INDEX_CALL_SIGN = 2;
- private static final int INDEX_LOGO_URI = 3;
-
- private static final String COLUMN_CHANNEL_LOGO = "logo";
+ private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
+ "is_first_time_fetch_channel_logo";
- private static final Object sLock = new Object();
- private static final Set<Long> sChannelIdBlackListSet = new HashSet<>();
- private static LoadChannelTask sQueryTask;
private static FetchLogoTask sFetchTask;
/**
- * Fetch the channel logos from TMS data and insert them into TvProvider.
+ * Fetches the channel logos from the cloud data and insert them into TvProvider.
* The previous task is canceled and a new task starts.
*/
- public static void startFetchingChannelLogos(Context context) {
+ @MainThread
+ public static void startFetchingChannelLogos(
+ Context context, List<Channel> channels) {
if (!PermissionUtils.hasAccessAllEpg(context)) {
// TODO: support this feature for non-system LC app. b/23939816
return;
}
- synchronized (sLock) {
- stopFetchingChannelLogos();
- if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
- sQueryTask = new LoadChannelTask(context);
- sQueryTask.executeOnDbThread();
+ if (sFetchTask != null) {
+ sFetchTask.cancel(true);
+ sFetchTask = null;
}
- }
-
- /**
- * 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.getApplicationContext(), channels);
+ sFetchTask.execute();
}
private ChannelLogoFetcher() {
}
- private static final class LoadChannelTask extends AsyncDbTask<Void, Void, List<Channel>> {
- private final Context mContext;
-
- public LoadChannelTask(Context context) {
- mContext = context;
- }
-
- @Override
- protected List<Channel> doInBackground(Void... arg) {
- // Load channels which doesn't have channel logos.
- if (DEBUG) Log.d(TAG, "Starts loading the channels from DB");
- String[] projection =
- new String[] { Channels._ID, Channels.COLUMN_DISPLAY_NAME };
- String selection = COLUMN_CHANNEL_LOGO + " IS NULL AND "
- + Channels.COLUMN_PACKAGE_NAME + "=?";
- String[] selectionArgs = new String[] { mContext.getPackageName() };
- try (Cursor c = mContext.getContentResolver().query(Channels.CONTENT_URI,
- projection, selection, selectionArgs, null)) {
- if (c == null) {
- Log.e(TAG, "Query returns null cursor", new RuntimeException());
- return null;
- }
- List<Channel> channels = new ArrayList<>();
- while (!isCancelled() && c.moveToNext()) {
- long channelId = c.getLong(0);
- if (sChannelIdBlackListSet.contains(channelId)) {
- continue;
- }
- channels.add(new Channel.Builder().setId(c.getLong(0))
- .setDisplayName(c.getString(1).toUpperCase(Locale.getDefault()))
- .build());
- }
- return channels;
- }
- }
-
- @Override
- protected void onPostExecute(List<Channel> channels) {
- synchronized (sLock) {
- if (DEBUG) {
- int count = channels == null ? 0 : channels.size();
- Log.d(TAG, count + " channels are loaded");
- }
- if (sQueryTask == this) {
- sQueryTask = null;
- if (channels != null && !channels.isEmpty()) {
- sFetchTask = new FetchLogoTask(mContext, channels);
- sFetchTask.execute();
- }
- }
- }
- }
- }
-
private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
private final Context mContext;
private final List<Channel> mChannels;
- public FetchLogoTask(Context context, List<Channel> channels) {
+ private FetchLogoTask(Context context, List<Channel> channels) {
mContext = context;
mChannels = channels;
}
@@ -180,83 +94,53 @@ public class ChannelLogoFetcher {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Load the TMS table data.
- if (DEBUG) Log.d(TAG, "Loads TMS data");
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- try {
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_US_TABLE_FILE));
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+ List<Channel> channelsToUpdate = new ArrayList<>();
+ List<Channel> channelsToRemove = new ArrayList<>();
+ // Updates or removes the logo by comparing the logo uri which is got from the cloud
+ // and the stored one. And we assume that the data got form the cloud is 100%
+ // correct and completed.
+ SharedPreferences sharedPreferences =
+ mContext.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
+ Context.MODE_PRIVATE);
+ SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
+ Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
+ boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean(
+ PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
+ // Iterating channels.
+ for (Channel channel : mChannels) {
+ String channelIdString = Long.toString(channel.getId());
+ String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
+ if (!TextUtils.isEmpty(channel.getLogoUri())
+ && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
+ channelsToUpdate.add(channel);
+ sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
+ } else if (TextUtils.isEmpty(channel.getLogoUri())
+ && (!TextUtils.isEmpty(storedChannelLogoUri)
+ || isFirstTimeFetchChannelLogo)) {
+ channelsToRemove.add(channel);
+ sharedPreferencesEditor.remove(channelIdString);
}
- channelNameLogoUriMap.putAll(readTmsFile(mContext, TMS_KR_TABLE_FILE));
- } catch (IOException e) {
- Log.e(TAG, "Loading TMS data failed.", e);
- return null;
}
- if (isCancelled()) {
- if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
- return null;
+
+ // Removes non existing channels from SharedPreferences.
+ for (String channelId : uncheckedChannels.keySet()) {
+ sharedPreferencesEditor.remove(channelId);
}
- // Iterating channels.
- for (Channel channel : mChannels) {
+ // Updates channel logos.
+ for (Channel channel : channelsToUpdate) {
if (isCancelled()) {
if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
return null;
}
- // Download the channel logo.
- if (TextUtils.isEmpty(channel.getDisplayName())) {
- if (DEBUG) {
- Log.d(TAG, "The channel with ID (" + channel.getId()
- + ") doesn't have the display name.");
- }
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
- String channelName = channel.getDisplayName().trim();
- String logoUri = channelNameLogoUriMap.get(channelName);
- if (TextUtils.isEmpty(logoUri)) {
- if (DEBUG) {
- Log.d(TAG, "Can't find a logo URI for channel '" + channelName + "'");
- }
- // Find the candidate names. If the channel name is CNN-HD, then find CNNHD
- // and CNN. Or if the channel name is KQED+, then find KQED.
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_DB);
- if (splitNames.length > 1) {
- StringBuilder sb = new StringBuilder();
- for (String splitName : splitNames) {
- sb.append(splitName);
- }
- logoUri = channelNameLogoUriMap.get(sb.toString());
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + sb.toString()
- + "'");
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)
- && splitNames[0].length() != channelName.length()) {
- logoUri = channelNameLogoUriMap.get(splitNames[0]);
- if (DEBUG) {
- if (TextUtils.isEmpty(logoUri)) {
- Log.d(TAG, "Can't find a logo URI for channel '" + splitNames[0]
- + "'");
- }
- }
- }
- }
- if (TextUtils.isEmpty(logoUri)) {
- sChannelIdBlackListSet.add(channel.getId());
- continue;
- }
+ // Downloads the channel logo.
+ String logoUri = channel.getLogoUri();
ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
if (bitmapInfo == null) {
Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
+ ", " + "logoUri=" + logoUri + "}");
- sChannelIdBlackListSet.add(channel.getId());
continue;
}
if (isCancelled()) {
@@ -264,12 +148,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 +164,35 @@ public class ChannelLogoFetcher {
+ dstLogoUri + "}");
}
}
- if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
- return null;
- }
- @WorkerThread
- private Map<String, String> readTmsFile(Context context, String fileName)
- throws IOException {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(
- context.getAssets().open(fileName)))) {
- Map<String, String> channelNameLogoUriMap = new HashMap<>();
- String line;
- while ((line = reader.readLine()) != null && !isCancelled()) {
- String[] data = line.split(FIELD_SEPARATOR);
- if (data.length != INDEX_LOGO_URI + 1) {
- if (DEBUG) Log.d(TAG, "Invalid or comment row: " + line);
- continue;
- }
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_SHORT_NAME].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
- addChannelNames(channelNameLogoUriMap,
- data[INDEX_CALL_SIGN].toUpperCase(Locale.getDefault()),
- data[INDEX_LOGO_URI]);
+ // Removes the logos for the channels that have logos before but now
+ // their logo uris are null.
+ boolean deleteChannelLogoFailed = false;
+ if (!channelsToRemove.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (Channel channel : channelsToRemove) {
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildChannelLogoUri(channel.getId())).build());
+ }
+ try {
+ mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ } catch (RemoteException | OperationApplicationException e) {
+ deleteChannelLogoFailed = true;
+ Log.e(TAG, "Error deleting obsolete channels", e);
}
- return channelNameLogoUriMap;
}
- }
-
- private void addChannelNames(Map<String, String> channelNameLogoUriMap, String channelName,
- String logoUri) {
- if (!TextUtils.isEmpty(channelName)) {
- channelNameLogoUriMap.put(channelName, logoUri);
- // Find the candidate names.
- // If the name is like "W05AAD (W05AA-D)", then split the names into "W05AAD" and
- // "W05AA-D"
- String[] splitNames = channelName.split(NAME_SEPARATOR_FOR_TMS);
- if (splitNames.length > 1) {
- for (String name : splitNames) {
- name = name.trim();
- if (channelNameLogoUriMap.get(name) == null) {
- channelNameLogoUriMap.put(name, logoUri);
- }
- }
- }
+ 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;
}
@Override
protected void onPostExecute(Void result) {
- synchronized (sLock) {
- if (sFetchTask == this) {
- sFetchTask = null;
- }
- }
+ sFetchTask = null;
}
}
}
diff --git a/src/com/android/tv/data/ChannelNumber.java b/src/com/android/tv/data/ChannelNumber.java
index 59021609..29054aa5 100644
--- a/src/com/android/tv/data/ChannelNumber.java
+++ b/src/com/android/tv/data/ChannelNumber.java
@@ -17,37 +17,38 @@
package com.android.tv.data;
import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.view.KeyEvent;
+import com.android.tv.util.StringUtils;
+
+import java.util.Objects;
+
/**
* A convenience class to handle channel number.
*/
public final class ChannelNumber implements Comparable<ChannelNumber> {
- public static final String PRIMARY_CHANNEL_DELIMITER = "-";
- public static final String[] CHANNEL_DELIMITERS = {"-", ".", " "};
-
private static final int[] CHANNEL_DELIMITER_KEYCODES = {
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_SPACE
};
+ /** The major part of the channel number. */
public String majorNumber;
+ /** The flag which indicates whether it has a delimiter or not. */
public boolean hasDelimiter;
+ /** The major part of the channel number. */
public String minorNumber;
public ChannelNumber() {
reset();
}
- public ChannelNumber(String major, boolean hasDelimiter, String minor) {
- setChannelNumber(major, hasDelimiter, minor);
- }
-
public void reset() {
setChannelNumber("", false, "");
}
- public void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
+ private void setChannelNumber(String majorNumber, boolean hasDelimiter, String minorNumber) {
this.majorNumber = majorNumber;
this.hasDelimiter = hasDelimiter;
this.minorNumber = minorNumber;
@@ -56,7 +57,7 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
@Override
public String toString() {
if (hasDelimiter) {
- return majorNumber + PRIMARY_CHANNEL_DELIMITER + minorNumber;
+ return majorNumber + Channel.CHANNEL_NUMBER_DELIMITER + minorNumber;
}
return majorNumber;
}
@@ -75,6 +76,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return major - opponentMajor;
}
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ChannelNumber) {
+ ChannelNumber channelNumber = (ChannelNumber) obj;
+ return TextUtils.equals(majorNumber, channelNumber.majorNumber)
+ && TextUtils.equals(minorNumber, channelNumber.minorNumber)
+ && hasDelimiter == channelNumber.hasDelimiter;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(majorNumber, hasDelimiter, minorNumber);
+ }
+
public static boolean isChannelNumberDelimiterKey(int keyCode) {
for (int delimiterKeyCode : CHANNEL_DELIMITER_KEYCODES) {
if (delimiterKeyCode == keyCode) {
@@ -84,22 +101,22 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return false;
}
+ /**
+ * Returns the ChannelNumber instance.
+ * <p>
+ * Note that all the channel number argument should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static ChannelNumber parseChannelNumber(String number) {
if (number == null) {
return null;
}
ChannelNumber ret = new ChannelNumber();
- int indexOfDelimiter = -1;
- for (String delimiter : CHANNEL_DELIMITERS) {
- indexOfDelimiter = number.indexOf(delimiter);
- if (indexOfDelimiter >= 0) {
- break;
- }
- }
+ int indexOfDelimiter = number.indexOf(Channel.CHANNEL_NUMBER_DELIMITER);
if (indexOfDelimiter == 0 || indexOfDelimiter == number.length() - 1) {
return null;
- }
- if (indexOfDelimiter < 0) {
+ } else if (indexOfDelimiter < 0) {
ret.majorNumber = number;
if (!isInteger(ret.majorNumber)) {
return null;
@@ -115,25 +132,31 @@ public final class ChannelNumber implements Comparable<ChannelNumber> {
return ret;
}
+ /**
+ * Compares the channel numbers.
+ * <p>
+ * Note that all the channel number arguments should be normalized by
+ * {@link Channel#normalizeDisplayNumber}. The channels retrieved from
+ * {@link ChannelDataManager} are already normalized.
+ */
public static int compare(String lhs, String rhs) {
ChannelNumber lhsNumber = parseChannelNumber(lhs);
ChannelNumber rhsNumber = parseChannelNumber(rhs);
+ // Null first
if (lhsNumber == null && rhsNumber == null) {
- return 0;
+ return StringUtils.compare(lhs, rhs);
} else if (lhsNumber == null /* && rhsNumber != null */) {
return -1;
- } else if (lhsNumber != null && rhsNumber == null) {
+ } else if (rhsNumber == null) {
return 1;
}
return lhsNumber.compareTo(rhsNumber);
}
- public static boolean isInteger(String string) {
+ private static boolean isInteger(String string) {
try {
Integer.parseInt(string);
- } catch(NumberFormatException e) {
- return false;
- } catch(NullPointerException e) {
+ } catch(NumberFormatException | NullPointerException e) {
return false;
}
return true;
diff --git a/src/com/android/tv/data/InternalDataUtils.java b/src/com/android/tv/data/InternalDataUtils.java
index 6054f089..e33ca18f 100644
--- a/src/com/android/tv/data/InternalDataUtils.java
+++ b/src/com/android/tv/data/InternalDataUtils.java
@@ -21,7 +21,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.tv.data.Program.CriticScore;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
diff --git a/src/com/android/tv/data/PreviewDataManager.java b/src/com/android/tv/data/PreviewDataManager.java
new file mode 100644
index 00000000..01a58520
--- /dev/null
+++ b/src/com/android/tv/data/PreviewDataManager.java
@@ -0,0 +1,636 @@
+/*
+ * 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.data;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.support.media.tv.ChannelLogoUtils;
+import android.support.media.tv.PreviewProgram;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.R;
+import com.android.tv.util.PermissionUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Class to manage the preview data.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+@MainThread
+public class PreviewDataManager {
+ private static final String TAG = "PreviewDataManager";
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
+
+ /**
+ * Invalid preview channel ID.
+ */
+ public static final long INVALID_PREVIEW_CHANNEL_ID = -1;
+ @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PreviewChannelType{}
+
+ /**
+ * Type of default preview channel
+ */
+ public static final long TYPE_DEFAULT_PREVIEW_CHANNEL = 1;
+ /**
+ * Type of recorded program channel
+ */
+ public static final long TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2;
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private boolean mLoadFinished;
+ private PreviewData mPreviewData = new PreviewData();
+ private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
+
+ private QueryPreviewDataTask mQueryPreviewTask;
+ private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks =
+ new HashMap<>();
+ private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>();
+
+ private final int mPreviewChannelLogoWidth;
+ private final int mPreviewChannelLogoHeight;
+
+ public PreviewDataManager(Context context) {
+ mContext = context.getApplicationContext();
+ mContentResolver = context.getContentResolver();
+ mPreviewChannelLogoWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.preview_channel_logo_width);
+ mPreviewChannelLogoHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.preview_channel_logo_height);
+ }
+
+ /**
+ * Starts the preview data manager.
+ */
+ public void start() {
+ if (mQueryPreviewTask == null) {
+ mQueryPreviewTask = new QueryPreviewDataTask();
+ mQueryPreviewTask.execute();
+ }
+ }
+
+ /**
+ * Stops the preview data manager.
+ */
+ public void stop() {
+ if (mQueryPreviewTask != null) {
+ mQueryPreviewTask.cancel(true);
+ }
+ for (CreatePreviewChannelTask createPreviewChannelTask
+ : mCreatePreviewChannelTasks.values()) {
+ createPreviewChannelTask.cancel(true);
+ }
+ for (UpdatePreviewProgramTask updatePreviewProgramTask
+ : mUpdatePreviewProgramTasks.values()) {
+ updatePreviewProgramTask.cancel(true);
+ }
+
+ mQueryPreviewTask = null;
+ mCreatePreviewChannelTasks.clear();
+ mUpdatePreviewProgramTasks.clear();
+ }
+
+ /**
+ * Gets preview channel ID from the preview channel type.
+ */
+ public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
+ return mPreviewData.getPreviewChannelId(previewChannelType);
+ }
+
+ /**
+ * Creates default preview channel.
+ */
+ public void createDefaultPreviewChannel(
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener);
+ }
+
+ /**
+ * Creates a preview channel for specific channel type.
+ */
+ public void createPreviewChannel(@PreviewChannelType long previewChannelType,
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ CreatePreviewChannelTask currentRunningCreateTask =
+ mCreatePreviewChannelTasks.get(previewChannelType);
+ if (currentRunningCreateTask == null) {
+ CreatePreviewChannelTask createPreviewChannelTask = new CreatePreviewChannelTask(
+ previewChannelType);
+ createPreviewChannelTask.addOnPreviewChannelCreationResultListener(
+ onPreviewChannelCreationResultListener);
+ createPreviewChannelTask.execute();
+ mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask);
+ } else {
+ currentRunningCreateTask.addOnPreviewChannelCreationResultListener(
+ onPreviewChannelCreationResultListener);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the preview data is loaded.
+ */
+ public boolean isLoadFinished() {
+ return mLoadFinished;
+ }
+
+ /**
+ * Adds listener.
+ */
+ public void addListener(PreviewDataListener previewDataListener) {
+ mPreviewDataListeners.add(previewDataListener);
+ }
+
+ /**
+ * Removes listener.
+ */
+ public void removeListener(PreviewDataListener previewDataListener) {
+ mPreviewDataListeners.remove(previewDataListener);
+ }
+
+ /**
+ * Updates the preview programs table for a specific preview channel.
+ */
+ public void updatePreviewProgramsForChannel(long previewChannelId,
+ Set<PreviewProgramContent> programs, PreviewDataListener previewDataListener) {
+ UpdatePreviewProgramTask currentRunningUpdateTask =
+ mUpdatePreviewProgramTasks.get(previewChannelId);
+ if (currentRunningUpdateTask != null
+ && currentRunningUpdateTask.getPrograms().equals(programs)) {
+ currentRunningUpdateTask.addPreviewDataListener(previewDataListener);
+ return;
+ }
+ UpdatePreviewProgramTask updatePreviewProgramTask =
+ new UpdatePreviewProgramTask(previewChannelId, programs);
+ updatePreviewProgramTask.addPreviewDataListener(previewDataListener);
+ if (currentRunningUpdateTask != null) {
+ currentRunningUpdateTask.cancel(true);
+ currentRunningUpdateTask.saveStatus();
+ updatePreviewProgramTask.addPreviewDataListeners(
+ currentRunningUpdateTask.getPreviewDataListeners());
+ }
+ updatePreviewProgramTask.execute();
+ mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask);
+ }
+
+ private void notifyPreviewDataLoadFinished() {
+ for (PreviewDataListener l : mPreviewDataListeners) {
+ l.onPreviewDataLoadFinished();
+ }
+ }
+
+ public interface PreviewDataListener {
+ /**
+ * Called when the preview data is loaded.
+ */
+ void onPreviewDataLoadFinished();
+
+ /**
+ * Called when the preview data is updated.
+ */
+ void onPreviewDataUpdateFinished();
+ }
+
+ public interface OnPreviewChannelCreationResultListener {
+ /**
+ * Called when the creation of preview channel is finished.
+ * @param createdPreviewChannelId The preview channel ID if created successfully,
+ * otherwise it's {@value #INVALID_PREVIEW_CHANNEL_ID}.
+ */
+ void onPreviewChannelCreationResult(long createdPreviewChannelId);
+ }
+
+ private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> {
+ private final String PARAM_PREVIEW = "preview";
+ private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?";
+
+ @Override
+ protected PreviewData doInBackground(Void... voids) {
+ // Query preview channels and programs.
+ if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground");
+ PreviewData previewData = new PreviewData();
+ try {
+ Uri previewChannelsUri =
+ PreviewDataUtils.addQueryParamToUri(
+ TvContract.Channels.CONTENT_URI,
+ new Pair<>(PARAM_PREVIEW, String.valueOf(true)));
+ String packageName = mContext.getPackageName();
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ try (Cursor cursor =
+ mContentResolver.query(
+ previewChannelsUri,
+ android.support.media.tv.Channel.PROJECTION,
+ mChannelSelection,
+ new String[] {packageName},
+ null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ android.support.media.tv.Channel previewChannel =
+ android.support.media.tv.Channel.fromCursor(cursor);
+ Long previewChannelType = previewChannel.getInternalProviderFlag1();
+ if (previewChannelType != null) {
+ previewData.addPreviewChannelId(
+ previewChannelType, previewChannel.getId());
+ }
+ }
+ }
+ }
+ } else {
+ try (Cursor cursor =
+ mContentResolver.query(
+ previewChannelsUri,
+ android.support.media.tv.Channel.PROJECTION,
+ null,
+ null,
+ null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ android.support.media.tv.Channel previewChannel =
+ android.support.media.tv.Channel.fromCursor(cursor);
+ Long previewChannelType = previewChannel.getInternalProviderFlag1();
+ if (previewChannel.getPackageName() == packageName
+ && previewChannelType != null) {
+ previewData.addPreviewChannelId(
+ previewChannelType, previewChannel.getId());
+ }
+ }
+ }
+ }
+ }
+
+ for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) {
+ Uri previewProgramsUriForPreviewChannel =
+ TvContract.buildPreviewProgramsUriForChannel(previewChannelId);
+ try (Cursor previewProgramCursor =
+ mContentResolver.query(
+ previewProgramsUriForPreviewChannel,
+ PreviewProgram.PROJECTION,
+ null,
+ null,
+ null)) {
+ if (previewProgramCursor != null) {
+ while (previewProgramCursor.moveToNext()) {
+ PreviewProgram previewProgram =
+ PreviewProgram.fromCursor(previewProgramCursor);
+ previewData.addPreviewProgram(previewProgram);
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ Log.w(TAG, "Unable to get preview data", e);
+ }
+ return previewData;
+ }
+
+ @Override
+ protected void onPostExecute(PreviewData result) {
+ super.onPostExecute(result);
+ if (mQueryPreviewTask == this) {
+ mQueryPreviewTask = null;
+ mPreviewData = new PreviewData(result);
+ mLoadFinished = true;
+ notifyPreviewDataLoadFinished();
+ }
+ }
+ }
+
+ private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> {
+ private final long mPreviewChannelType;
+ private Set<OnPreviewChannelCreationResultListener>
+ mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>();
+
+ public CreatePreviewChannelTask(long previewChannelType) {
+ mPreviewChannelType = previewChannelType;
+ }
+
+ public void addOnPreviewChannelCreationResultListener(
+ OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) {
+ if (onPreviewChannelCreationResultListener != null) {
+ mOnPreviewChannelCreationResultListeners.add(
+ onPreviewChannelCreationResultListener);
+ }
+ }
+
+ @Override
+ protected Long doInBackground(Void... params) {
+ if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground");
+ long previewChannelId;
+ try {
+ Uri channelUri = mContentResolver.insert(TvContract.Channels.CONTENT_URI,
+ PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType)
+ .toContentValues());
+ if (channelUri != null) {
+ previewChannelId = ContentUris.parseId(channelUri);
+ } else {
+ Log.e(TAG, "Fail to insert preview channel");
+ return INVALID_PREVIEW_CHANNEL_ID;
+ }
+ } catch (UnsupportedOperationException | NumberFormatException e) {
+ Log.e(TAG, "Fail to get channel ID");
+ return INVALID_PREVIEW_CHANNEL_ID;
+ }
+ Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager());
+ if (appIcon != null && appIcon instanceof BitmapDrawable) {
+ ChannelLogoUtils.storeChannelLogo(mContext, previewChannelId,
+ Bitmap.createScaledBitmap(((BitmapDrawable) appIcon).getBitmap(),
+ mPreviewChannelLogoWidth, mPreviewChannelLogoHeight, false));
+ }
+ return previewChannelId;
+ }
+
+ @Override
+ protected void onPostExecute(Long result) {
+ super.onPostExecute(result);
+ if (result != INVALID_PREVIEW_CHANNEL_ID) {
+ mPreviewData.addPreviewChannelId(mPreviewChannelType, result);
+ }
+ for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener
+ : mOnPreviewChannelCreationResultListeners) {
+ onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result);
+ }
+ mCreatePreviewChannelTasks.remove(mPreviewChannelType);
+ }
+ }
+
+ /**
+ * Updates the whole data which belongs to the package in preview programs table for a
+ * specific preview channel with a set of {@link PreviewProgramContent}.
+ */
+ private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> {
+ private long mPreviewChannelId;
+ private Set<PreviewProgramContent> mPrograms;
+ private Map<Long, Long> mCurrentProgramId2PreviewProgramId;
+ private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>();
+
+ public UpdatePreviewProgramTask(long previewChannelId,
+ Set<PreviewProgramContent> programs) {
+ mPreviewChannelId = previewChannelId;
+ mPrograms = programs;
+ if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) {
+ mCurrentProgramId2PreviewProgramId = new HashMap<>();
+ } else {
+ mCurrentProgramId2PreviewProgramId = new HashMap<>(
+ mPreviewData.getPreviewProgramIds(previewChannelId));
+ }
+ }
+
+ public void addPreviewDataListener(PreviewDataListener previewDataListener) {
+ if (previewDataListener != null) {
+ mPreviewDataListeners.add(previewDataListener);
+ }
+ }
+
+ public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) {
+ if (previewDataListeners != null) {
+ mPreviewDataListeners.addAll(previewDataListeners);
+ }
+ }
+
+ public Set<PreviewProgramContent> getPrograms() {
+ return mPrograms;
+ }
+
+ public Set<PreviewDataListener> getPreviewDataListeners() {
+ return mPreviewDataListeners;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground");
+ Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId);
+ for (PreviewProgramContent program : mPrograms) {
+ if (isCancelled()) {
+ return null;
+ }
+ Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId());
+ if (existingPreviewProgramId != null) {
+ if (DEBUG) Log.d(TAG, "Preview program " + existingPreviewProgramId + " " +
+ "already exists for program " + program.getId());
+ continue;
+ }
+ try {
+ Uri programUri = mContentResolver.insert(TvContract.PreviewPrograms.CONTENT_URI,
+ PreviewDataUtils.createPreviewProgramFromContent(program)
+ .toContentValues());
+ if (programUri != null) {
+ long previewProgramId = ContentUris.parseId(programUri);
+ mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId);
+ if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId);
+ } else {
+ Log.e(TAG, "Fail to insert preview program");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to get preview program ID");
+ }
+ }
+
+ for (Long key : uncheckedPrograms.keySet()) {
+ if (isCancelled()) {
+ return null;
+ }
+ try {
+ if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key));
+ mContentResolver.delete(TvContract.buildPreviewProgramUri(
+ uncheckedPrograms.get(key)), null, null);
+ mCurrentProgramId2PreviewProgramId.remove(key);
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ mPreviewData.setPreviewProgramIds(
+ mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
+ mUpdatePreviewProgramTasks.remove(mPreviewChannelId);
+ for (PreviewDataListener previewDataListener : mPreviewDataListeners) {
+ previewDataListener.onPreviewDataUpdateFinished();
+ }
+ }
+
+ public void saveStatus() {
+ mPreviewData.setPreviewProgramIds(
+ mPreviewChannelId, mCurrentProgramId2PreviewProgramId);
+ }
+ }
+
+ /**
+ * Class to store the query result of preview data.
+ */
+ private static final class PreviewData {
+ private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>();
+ private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>();
+
+ PreviewData() {
+ mPreviewChannelType2Id = new HashMap<>();
+ mProgramId2PreviewProgramId = new HashMap<>();
+ }
+
+ PreviewData(PreviewData previewData) {
+ mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id);
+ mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId);
+ }
+
+ public void addPreviewProgram(PreviewProgram previewProgram) {
+ long previewChannelId = previewProgram.getChannelId();
+ Map<Long, Long> programId2PreviewProgram =
+ mProgramId2PreviewProgramId.get(previewChannelId);
+ if (programId2PreviewProgram == null) {
+ programId2PreviewProgram = new HashMap<>();
+ }
+ mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram);
+ if (previewProgram.getInternalProviderId() != null) {
+ programId2PreviewProgram.put(
+ Long.parseLong(previewProgram.getInternalProviderId()),
+ previewProgram.getId());
+ }
+ }
+
+ public @PreviewChannelType long getPreviewChannelId(long previewChannelType) {
+ Long result = mPreviewChannelType2Id.get(previewChannelType);
+ return result == null ? INVALID_PREVIEW_CHANNEL_ID : result;
+ }
+
+ public Map<Long, Long> getAllPreviewChannelIds() {
+ return mPreviewChannelType2Id;
+ }
+
+ public void addPreviewChannelId(long previewChannelType, long previewChannelId) {
+ mPreviewChannelType2Id.put(previewChannelType, previewChannelId);
+ }
+
+ public void removePreviewChannelId(long previewChannelType) {
+ mPreviewChannelType2Id.remove(previewChannelType);
+ }
+
+ public void removePreviewChannel(long previewChannelId) {
+ removePreviewChannelId(previewChannelId);
+ removePreviewProgramIds(previewChannelId);
+ }
+
+ public Map<Long, Long> getPreviewProgramIds(long previewChannelId) {
+ return mProgramId2PreviewProgramId.get(previewChannelId);
+ }
+
+ public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() {
+ return mProgramId2PreviewProgramId;
+ }
+
+ public void setPreviewProgramIds(
+ long previewChannelId, Map<Long, Long> programId2PreviewProgramId) {
+ mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId);
+ }
+
+ public void removePreviewProgramIds(long previewChannelId) {
+ mProgramId2PreviewProgramId.remove(previewChannelId);
+ }
+ }
+
+ /**
+ * A utils class for preview data.
+ */
+ public final static class PreviewDataUtils {
+ /**
+ * Creates a preview channel.
+ */
+ public static android.support.media.tv.Channel createPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) {
+ return createRecordedProgramPreviewChannel(context, previewChannelType);
+ }
+ return createDefaultPreviewChannel(context, previewChannelType);
+ }
+
+ private static android.support.media.tv.Channel createDefaultPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ android.support.media.tv.Channel.Builder builder =
+ new android.support.media.tv.Channel.Builder();
+ CharSequence appLabel =
+ context.getApplicationInfo().loadLabel(context.getPackageManager());
+ CharSequence appDescription =
+ context.getApplicationInfo().loadDescription(context.getPackageManager());
+ builder.setType(TvContract.Channels.TYPE_PREVIEW)
+ .setDisplayName(appLabel == null ? null : appLabel.toString())
+ .setDescription(appDescription == null ? null : appDescription.toString())
+ .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
+ .setInternalProviderFlag1(previewChannelType);
+ return builder.build();
+ }
+
+ private static android.support.media.tv.Channel createRecordedProgramPreviewChannel(
+ Context context, @PreviewChannelType long previewChannelType) {
+ android.support.media.tv.Channel.Builder builder =
+ new android.support.media.tv.Channel.Builder();
+ builder.setType(TvContract.Channels.TYPE_PREVIEW)
+ .setDisplayName(context.getResources().getString(
+ R.string.recorded_programs_preview_channel))
+ .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI)
+ .setInternalProviderFlag1(previewChannelType);
+ return builder.build();
+ }
+
+ /**
+ * Creates a preview program.
+ */
+ public static PreviewProgram createPreviewProgramFromContent(
+ PreviewProgramContent program) {
+ PreviewProgram.Builder builder = new PreviewProgram.Builder();
+ builder.setChannelId(program.getPreviewChannelId())
+ .setType(program.getType())
+ .setLive(program.getLive())
+ .setTitle(program.getTitle())
+ .setDescription(program.getDescription())
+ .setPosterArtUri(program.getPosterArtUri())
+ .setIntentUri(program.getIntentUri())
+ .setPreviewVideoUri(program.getPreviewVideoUri())
+ .setInternalProviderId(Long.toString(program.getId()));
+ return builder.build();
+ }
+
+ /**
+ * Appends query parameters to a Uri.
+ */
+ public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) {
+ return uri.buildUpon().appendQueryParameter(param.first, param.second).build();
+ }
+ }
+}
diff --git a/src/com/android/tv/data/PreviewProgramContent.java b/src/com/android/tv/data/PreviewProgramContent.java
new file mode 100644
index 00000000..39f5051d
--- /dev/null
+++ b/src/com/android/tv/data/PreviewProgramContent.java
@@ -0,0 +1,259 @@
+/*
+ * 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.data;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.data.RecordedProgram;
+
+import java.util.Objects;
+
+/**
+ * A class to store the content of preview programs.
+ */
+public class PreviewProgramContent {
+ private final static String PARAM_INPUT = "input";
+
+ private long mId;
+ private long mPreviewChannelId;
+ private int mType;
+ private boolean mLive;
+ private String mTitle;
+ private String mDescription;
+ private Uri mPosterArtUri;
+ private Uri mIntentUri;
+ private Uri mPreviewVideoUri;
+
+ /**
+ * Create preview program content from {@link Program}
+ */
+ public static PreviewProgramContent createFromProgram(Context context,
+ long previewChannelId, Program program) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(program.getChannelId());
+ if (channel == null) {
+ return null;
+ }
+ String channelDisplayName = channel.getDisplayName();
+ return new PreviewProgramContent.Builder()
+ .setId(program.getId())
+ .setPreviewChannelId(previewChannelId)
+ .setType(TvContract.PreviewPrograms.TYPE_CHANNEL)
+ .setLive(true)
+ .setTitle(program.getTitle())
+ .setDescription(!TextUtils.isEmpty(channelDisplayName)
+ ? channelDisplayName : channel.getDisplayNumber())
+ .setPosterArtUri(Uri.parse(program.getPosterArtUri()))
+ .setIntentUri(channel.getUri())
+ .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+ channel.getUri(), new Pair<>(PARAM_INPUT, channel.getInputId())))
+ .build();
+ }
+
+ /**
+ * Create preview program content from {@link RecordedProgram}
+ */
+ public static PreviewProgramContent createFromRecordedProgram(
+ Context context, long previewChannelId, RecordedProgram recordedProgram) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(recordedProgram.getChannelId());
+ String channelDisplayName = null;
+ if (channel != null) {
+ channelDisplayName = channel.getDisplayName();
+ }
+ Uri recordedProgramUri = TvContract.buildRecordedProgramUri(recordedProgram.getId());
+ return new PreviewProgramContent.Builder()
+ .setId(recordedProgram.getId())
+ .setPreviewChannelId(previewChannelId)
+ .setType(TvContract.PreviewPrograms.TYPE_CLIP)
+ .setTitle(recordedProgram.getTitle())
+ .setDescription(channelDisplayName != null ? channelDisplayName : "")
+ .setPosterArtUri(Uri.parse(recordedProgram.getPosterArtUri()))
+ .setIntentUri(recordedProgramUri)
+ .setPreviewVideoUri(PreviewDataManager.PreviewDataUtils.addQueryParamToUri(
+ recordedProgramUri, new Pair<>(PARAM_INPUT, recordedProgram.getInputId())))
+ .build();
+ }
+
+ private PreviewProgramContent() { }
+
+ public void copyFrom(PreviewProgramContent other) {
+ if (this == other) {
+ return;
+ }
+ mId = other.mId;
+ mPreviewChannelId = other.mPreviewChannelId;
+ mType = other.mType;
+ mLive = other.mLive;
+ mTitle = other.mTitle;
+ mDescription = other.mDescription;
+ mPosterArtUri = other.mPosterArtUri;
+ mIntentUri = other.mIntentUri;
+ mPreviewVideoUri = other.mPreviewVideoUri;
+ }
+
+ /**
+ * Returns the id, which is an identification. It usually comes from the original data which
+ * create the {@PreviewProgramContent}.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the preview channel id which the preview program belongs to.
+ */
+ public long getPreviewChannelId() {
+ return mPreviewChannelId;
+ }
+
+ /**
+ * Returns the type of the preview program.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns whether the preview program is live or not.
+ */
+ public boolean getLive() {
+ return mLive;
+ }
+
+ /**
+ * Returns the title of the preview program.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the description of the preview program.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the poster art uri of the preview program.
+ */
+ public Uri getPosterArtUri() {
+ return mPosterArtUri;
+ }
+
+ /**
+ * Returns the intent uri of the preview program.
+ */
+ public Uri getIntentUri() {
+ return mIntentUri;
+ }
+
+ /**
+ * Returns the preview video uri of the preview program.
+ */
+ public Uri getPreviewVideoUri() {
+ return mPreviewVideoUri;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof PreviewProgramContent)) {
+ return false;
+ }
+ PreviewProgramContent previewProgramContent = (PreviewProgramContent) other;
+ return previewProgramContent.mId == mId
+ && previewProgramContent.mPreviewChannelId == mPreviewChannelId
+ && previewProgramContent.mType == mType
+ && previewProgramContent.mLive == mLive
+ && Objects.equals(previewProgramContent.mTitle, mTitle)
+ && Objects.equals(previewProgramContent.mDescription, mDescription)
+ && Objects.equals(previewProgramContent.mPosterArtUri, mPosterArtUri)
+ && Objects.equals(previewProgramContent.mIntentUri, mIntentUri)
+ && Objects.equals(previewProgramContent.mPreviewVideoUri, mPreviewVideoUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mPreviewChannelId, mType, mLive, mTitle, mDescription,
+ mPosterArtUri, mIntentUri, mPreviewVideoUri);
+ }
+
+ public static final class Builder {
+ private final PreviewProgramContent mPreviewProgramContent;
+
+ public Builder() {
+ mPreviewProgramContent = new PreviewProgramContent();
+ }
+
+ public Builder setId(long id) {
+ mPreviewProgramContent.mId = id;
+ return this;
+ }
+
+ public Builder setPreviewChannelId(long previewChannelId) {
+ mPreviewProgramContent.mPreviewChannelId = previewChannelId;
+ return this;
+ }
+
+ public Builder setType(int type) {
+ mPreviewProgramContent.mType = type;
+ return this;
+ }
+
+ public Builder setLive(boolean live) {
+ mPreviewProgramContent.mLive = live;
+ return this;
+ }
+
+ public Builder setTitle(String title) {
+ mPreviewProgramContent.mTitle = title;
+ return this;
+ }
+
+ public Builder setDescription(String description) {
+ mPreviewProgramContent.mDescription = description;
+ return this;
+ }
+
+ public Builder setPosterArtUri(Uri posterArtUri) {
+ mPreviewProgramContent.mPosterArtUri = posterArtUri;
+ return this;
+ }
+
+ public Builder setIntentUri(Uri intentUri) {
+ mPreviewProgramContent.mIntentUri = intentUri;
+ return this;
+ }
+
+ public Builder setPreviewVideoUri(Uri previewVideoUri) {
+ mPreviewProgramContent.mPreviewVideoUri = previewVideoUri;
+ return this;
+ }
+
+ public PreviewProgramContent build() {
+ PreviewProgramContent previewProgramContent = new PreviewProgramContent();
+ previewProgramContent.copyFrom(mPreviewProgramContent);
+ return previewProgramContent;
+ }
+ }
+}
diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java
index b9cd3d8d..071c7024 100644
--- a/src/com/android/tv/data/Program.java
+++ b/src/com/android/tv/data/Program.java
@@ -16,21 +16,23 @@
package com.android.tv.data;
+import android.annotation.SuppressLint;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
-import android.support.v4.os.BuildCompat;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.R;
import com.android.tv.common.BuildConfig;
import com.android.tv.common.CollectionUtils;
import com.android.tv.common.TvContentRatingCache;
@@ -88,9 +90,11 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
public static final String[] PROJECTION = createProjection();
private static String[] createProjection() {
- return CollectionUtils
- .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC
- : PROJECTION_DEPRECATED_IN_NYC);
+ return CollectionUtils.concatAll(
+ PROJECTION_BASE,
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ ? PROJECTION_ADDED_IN_NYC
+ : PROJECTION_DEPRECATED_IN_NYC);
}
/**
@@ -135,7 +139,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
}
index++;
- if (BuildCompat.isAtLeastN()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setSeasonNumber(cursor.getString(index++));
builder.setSeasonTitle(cursor.getString(index++));
builder.setEpisodeNumber(cursor.getString(index++));
@@ -213,11 +217,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
private TvContentRating[] mContentRatings;
private boolean mRecordingProhibited;
- /**
- * TODO(DVR): Need to fill the following data.
- */
- private boolean mRecordingScheduled;
-
private Program() {
// Do nothing.
}
@@ -268,46 +267,12 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
/**
* Returns the episode title.
*/
- public String getEpisodeTitle() {
- return mEpisodeTitle;
- }
-
- /**
- * Returns season number, episode number and episode title for display.
- */
@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);
- }
- }
+ public String getEpisodeTitle() {
return mEpisodeTitle;
}
@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);
- }
- }
-
- @Override
public String getSeasonNumber() {
return mSeasonNumber;
}
@@ -361,6 +326,8 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return mCriticScores;
}
+ @Nullable
+ @Override
public TvContentRating[] getContentRatings() {
return mContentRatings;
}
@@ -495,6 +462,63 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
return builder.append("}").toString();
}
+ /**
+ * Translates a {@link Program} to {@link ContentValues} that are ready to be written into
+ * Database.
+ */
+ @SuppressLint("InlinedApi")
+ @SuppressWarnings("deprecation")
+ public 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());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
+ program.getSeasonNumber());
+ putValue(values, TvContract.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, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription());
+ putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription());
+ putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri());
+ putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri());
+ String[] canonicalGenres = program.getCanonicalGenres();
+ if (canonicalGenres != null && canonicalGenres.length > 0) {
+ putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
+ TvContract.Programs.Genres.encode(canonicalGenres));
+ } else {
+ putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
+ }
+ putValue(values, Programs.COLUMN_CONTENT_RATING,
+ TvContentRatingCache.contentRatingsToString(program.getContentRatings()));
+ 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,
+ InternalDataUtils.serializeInternalProviderData(program));
+ return values;
+ }
+
+ private static void putValue(ContentValues contentValues, String key, String value) {
+ if (TextUtils.isEmpty(value)) {
+ contentValues.putNull(key);
+ } else {
+ contentValues.put(key, value);
+ }
+ }
+
+ private static void putValue(ContentValues contentValues, String key, byte[] value) {
+ if (value == null || value.length == 0) {
+ contentValues.putNull(key);
+ } else {
+ contentValues.put(key, value);
+ }
+ }
+
public void copyFrom(Program other) {
if (this == other) {
return;
@@ -524,13 +548,6 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
}
/**
- * Checks whether the program is episodic or not.
- */
- public boolean isEpisodic() {
- return mSeriesId != null;
- }
-
- /**
* A Builder for the Program class
*/
public static final class Builder {
@@ -799,8 +816,12 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
*/
public Program build() {
// Generate the series ID for the episodic program of other TV input.
- if (TextUtils.isEmpty(mProgram.mSeriesId)
+ if (TextUtils.isEmpty(mProgram.mTitle)) {
+ // If title is null, series cannot be generated for this program.
+ setSeriesId(null);
+ } else if (TextUtils.isEmpty(mProgram.mSeriesId)
&& !TextUtils.isEmpty(mProgram.mEpisodeNumber)) {
+ // If series ID is not set, generate it for the episodic program of other TV input.
setSeriesId(BaseProgram.generateSeriesId(mProgram.mPackageName, mProgram.mTitle));
}
Program program = new Program();
@@ -820,17 +841,20 @@ public final class Program extends BaseProgram implements Comparable<Program>, P
}
/**
- * Loads the program poster art and returns it via {@code callback}.<p>
+ * Loads the program poster art and returns it via {@code callback}.
* <p>
* Note that it may directly call {@code callback} if the program poster art already is loaded.
+ *
+ * @return {@code true} if the load is complete and the callback is executed.
*/
@UiThread
- public void loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
+ public boolean loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
ImageLoader.ImageLoaderCallback callback) {
if (mPosterArtUri == null) {
- return;
+ return false;
}
- ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
+ return ImageLoader.loadBitmap(
+ context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
}
public static boolean isDuplicate(Program p1, Program p2) {
diff --git a/src/com/android/tv/data/ProgramDataManager.java b/src/com/android/tv/data/ProgramDataManager.java
index d2af33a7..8cb5e74a 100644
--- a/src/com/android/tv/data/ProgramDataManager.java
+++ b/src/com/android/tv/data/ProgramDataManager.java
@@ -26,6 +26,7 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
@@ -35,8 +36,6 @@ import android.util.LruCache;
import com.android.tv.common.MemoryManageable;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.data.epg.EpgFetcher;
-import com.android.tv.experiments.Experiments;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Clock;
import com.android.tv.util.MultiLongSparseArray;
@@ -51,6 +50,7 @@ import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@MainThread
@@ -85,10 +85,12 @@ public class ProgramDataManager implements MemoryManageable {
private final Clock mClock;
private final ContentResolver mContentResolver;
private boolean mStarted;
+ // Updated only on the main thread.
+ private volatile boolean mCurrentProgramsLoadFinished;
private ProgramsUpdateTask mProgramsUpdateTask;
private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
new LongSparseArray<>();
- private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>();
+ private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>();
private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
private final Handler mHandler;
@@ -109,17 +111,14 @@ public class ProgramDataManager implements MemoryManageable {
private boolean mPauseProgramUpdate = false;
private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
- private final EpgFetcher mEpgFetcher;
+ @MainThread
public ProgramDataManager(Context context) {
- this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper(),
- EpgFetcher.getInstance(context));
+ this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
}
@VisibleForTesting
- ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper,
- EpgFetcher epgFetcher) {
- mEpgFetcher = epgFetcher;
+ ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) {
mClock = time;
mContentResolver = contentResolver;
mHandler = new MyHandler(looper);
@@ -175,9 +174,6 @@ public class ProgramDataManager implements MemoryManageable {
}
mContentResolver.registerContentObserver(Programs.CONTENT_URI,
true, mProgramObserver);
- if (mEpgFetcher != null && Experiments.CLOUD_EPG.get()) {
- mEpgFetcher.start();
- }
}
/**
@@ -190,10 +186,6 @@ public class ProgramDataManager implements MemoryManageable {
return;
}
mStarted = false;
-
- if (mEpgFetcher != null) {
- mEpgFetcher.stop();
- }
mContentResolver.unregisterContentObserver(mProgramObserver);
mHandler.removeCallbacksAndMessages(null);
@@ -205,13 +197,23 @@ public class ProgramDataManager implements MemoryManageable {
}
}
- /**
- * Returns the current program at the specified channel.
- */
+ @AnyThread
+ public boolean isCurrentProgramsLoadFinished() {
+ return mCurrentProgramsLoadFinished;
+ }
+
+ /** Returns the current program at the specified channel. */
+ @AnyThread
public Program getCurrentProgram(long channelId) {
return mChannelIdCurrentProgramMap.get(channelId);
}
+ /** Returns all the current programs. */
+ @AnyThread
+ public List<Program> getCurrentPrograms() {
+ return new ArrayList<>(mChannelIdCurrentProgramMap.values());
+ }
+
/**
* Reloads program data.
*/
@@ -338,19 +340,19 @@ public class ProgramDataManager implements MemoryManageable {
}
private void notifyCurrentProgramUpdate(long channelId, Program program) {
-
for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
.get(channelId)) {
listener.onCurrentProgramUpdated(channelId, program);
- }
+ }
for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
.get(Channel.INVALID_ID)) {
listener.onCurrentProgramUpdated(channelId, program);
- }
+ }
}
private void updateCurrentProgram(long channelId, Program program) {
- Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program);
+ Program previousProgram = program == null ? mChannelIdCurrentProgramMap.remove(channelId)
+ : mChannelIdCurrentProgramMap.put(channelId, program);
if (!Objects.equals(program, previousProgram)) {
if (mPrefetchEnabled) {
removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
@@ -581,22 +583,22 @@ public class ProgramDataManager implements MemoryManageable {
protected void onPostExecute(List<Program> programs) {
if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done");
mProgramsUpdateTask = null;
- if (programs == null) {
- return;
- }
- Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
- for (Program program : programs) {
- long channelId = program.getChannelId();
- updateCurrentProgram(channelId, program);
- removedChannelIds.remove(channelId);
- }
- for (Long channelId : removedChannelIds) {
- if (mPrefetchEnabled) {
- mChannelIdProgramCache.remove(channelId);
+ if (programs != null) {
+ Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
+ for (Program program : programs) {
+ long channelId = program.getChannelId();
+ updateCurrentProgram(channelId, program);
+ removedChannelIds.remove(channelId);
+ }
+ for (Long channelId : removedChannelIds) {
+ if (mPrefetchEnabled) {
+ mChannelIdProgramCache.remove(channelId);
+ }
+ mChannelIdCurrentProgramMap.remove(channelId);
+ notifyCurrentProgramUpdate(channelId, null);
}
- mChannelIdCurrentProgramMap.remove(channelId);
- notifyCurrentProgramUpdate(channelId, null);
}
+ mCurrentProgramsLoadFinished = true;
}
}
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/WatchedHistoryManager.java b/src/com/android/tv/data/WatchedHistoryManager.java
index 59319338..3edd7b1a 100644
--- a/src/com/android/tv/data/WatchedHistoryManager.java
+++ b/src/com/android/tv/data/WatchedHistoryManager.java
@@ -10,15 +10,14 @@ import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.common.SharedPreferencesUtils;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
@@ -28,15 +27,15 @@ import java.util.concurrent.TimeUnit;
*
* <p>When there is no access to watched table of TvProvider,
* this class is used to build up watched history and to compute recent channels.
+ * <p>Note that this class is not thread safe. Please use this on one thread.
*/
public class WatchedHistoryManager {
private final static String TAG = "WatchedHistoryManager";
- private final boolean DEBUG = false;
+ private final static boolean DEBUG = false;
private static final int MAX_HISTORY_SIZE = 10000;
private static final String PREF_KEY_LAST_INDEX = "last_index";
private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
- private static final long RECENT_CHANNEL_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
private final List<WatchedRecord> mWatchedHistory = new ArrayList<>();
private final List<WatchedRecord> mPendingRecords = new ArrayList<>();
@@ -92,11 +91,7 @@ public class WatchedHistoryManager {
WatchedHistoryManager(Context context, int maxHistorySize) {
mContext = context.getApplicationContext();
mMaxHistorySize = maxHistorySize;
- if (Looper.myLooper() == null) {
- mHandler = new Handler(Looper.getMainLooper());
- } else {
- mHandler = new Handler();
- }
+ mHandler = new Handler();
}
/**
@@ -107,56 +102,70 @@ public class WatchedHistoryManager {
return;
}
mStarted = true;
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- mSharedPreferences = mContext.getSharedPreferences(
- SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
- mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
- if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
- for (int i = 0; i <= mLastIndex; ++i) {
- WatchedRecord record =
- decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
- null));
- if (record != null) {
- mWatchedHistory.add(record);
- }
- }
- } else if (mLastIndex >= mMaxHistorySize) {
- for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
- WatchedRecord record = decode(mSharedPreferences.getString(
- getSharedPreferencesKey(i), null));
- if (record != null) {
- mWatchedHistory.add(record);
- }
- }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ loadWatchedHistory();
+ return null;
}
- return null;
- }
- @Override
- protected void onPostExecute(Void params) {
- mLoaded = true;
- if (DEBUG) {
- Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
+ @Override
+ protected void onPostExecute(Void params) {
+ onLoadFinished();
}
- if (!mPendingRecords.isEmpty()) {
- Editor editor = mSharedPreferences.edit();
- for (WatchedRecord record : mPendingRecords) {
- mWatchedHistory.add(record);
- ++mLastIndex;
- editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
- }
- editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
- mPendingRecords.clear();
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loadWatchedHistory();
+ onLoadFinished();
+ }
+ }
+
+ @WorkerThread
+ private void loadWatchedHistory() {
+ mSharedPreferences = mContext.getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
+ mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
+ if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
+ for (int i = 0; i <= mLastIndex; ++i) {
+ WatchedRecord record =
+ decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
+ null));
+ if (record != null) {
+ mWatchedHistory.add(record);
}
- if (mListener != null) {
- mListener.onLoadFinished();
+ }
+ } else if (mLastIndex >= mMaxHistorySize) {
+ for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
+ WatchedRecord record = decode(mSharedPreferences.getString(
+ getSharedPreferencesKey(i), null));
+ if (record != null) {
+ mWatchedHistory.add(record);
}
- mSharedPreferences.registerOnSharedPreferenceChangeListener(
- mOnSharedPreferenceChangeListener);
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ private void onLoadFinished() {
+ mLoaded = true;
+ if (DEBUG) {
+ Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
+ }
+ if (!mPendingRecords.isEmpty()) {
+ Editor editor = mSharedPreferences.edit();
+ for (WatchedRecord record : mPendingRecords) {
+ mWatchedHistory.add(record);
+ ++mLastIndex;
+ editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
+ }
+ editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
+ mPendingRecords.clear();
+ }
+ if (mListener != null) {
+ mListener.onLoadFinished();
+ }
+ mSharedPreferences.registerOnSharedPreferenceChangeListener(
+ mOnSharedPreferenceChangeListener);
}
@VisibleForTesting
@@ -204,52 +213,6 @@ public class WatchedHistoryManager {
return Collections.unmodifiableList(mWatchedHistory);
}
- /**
- * Returns the list of recently watched channels.
- */
- public List<Channel> buildRecentChannel(ChannelDataManager channelDataManager, int maxCount) {
- List<Channel> list = new ArrayList<>();
- Map<Long, Long> durationMap = new HashMap<>();
- for (int i = mWatchedHistory.size() - 1; i >= 0; --i) {
- WatchedRecord record = mWatchedHistory.get(i);
- long channelId = record.channelId;
- Channel channel = channelDataManager.getChannel(channelId);
- if (channel == null || !channel.isBrowsable()) {
- continue;
- }
- Long duration = durationMap.get(channelId);
- if (duration == null) {
- duration = 0L;
- }
- if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
- continue;
- }
- if (list.isEmpty()) {
- // We put the first recent channel regardless of RECENT_CHANNEL_THREASHOLD.
- // It has the similar functionality as the previous channel in a usual remote
- // controller.
- list.add(channel);
- durationMap.put(channelId, RECENT_CHANNEL_THRESHOLD_MS);
- } else {
- duration += record.duration;
- durationMap.put(channelId, duration);
- if (duration >= RECENT_CHANNEL_THRESHOLD_MS) {
- list.add(channel);
- }
- }
- if (list.size() >= maxCount) {
- break;
- }
- }
- if (DEBUG) {
- Log.d(TAG, "Build recent channel");
- for (Channel channel : list) {
- Log.d(TAG, "recent channel: " + channel);
- }
- }
- return list;
- }
-
@VisibleForTesting
WatchedRecord getRecord(int reverseIndex) {
return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
diff --git a/src/com/android/tv/data/epg/EpgFetchHelper.java b/src/com/android/tv/data/epg/EpgFetchHelper.java
new file mode 100644
index 00000000..5693c877
--- /dev/null
+++ b/src/com/android/tv/data/epg/EpgFetchHelper.java
@@ -0,0 +1,233 @@
+/*
+ * 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.data.epg;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.data.Program;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** The helper class for {@link com.android.tv.data.epg.EpgFetcher} */
+class EpgFetchHelper {
+ private static final String TAG = "EpgFetchHelper";
+ private static final boolean DEBUG = false;
+
+ private static final long PROGRAM_QUERY_DURATION_MS = TimeUnit.DAYS.toMillis(30);
+ private static final int BATCH_OPERATION_COUNT = 100;
+
+ // Value: Long
+ private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
+ "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
+ // Value: String
+ private static final String KEY_LAST_LINEUP_ID =
+ "com.android.tv.data.epg.EpgFetcher.LastLineupId";
+
+ private static long sLastEpgUpdatedTimestamp = -1;
+ private static String sLastLineupId;
+
+ private EpgFetchHelper() { }
+
+ /**
+ * Updates newly fetched EPG data for the given channel to local providers. The method will
+ * compare the broadcasting time and try to match each newly fetched program with old programs
+ * of that channel in the database one by one. It will update the matched old program, or insert
+ * the new program if there is no matching program can be found in the database and at the same
+ * time remove those old programs which conflicts with the inserted one.
+
+ * @param channelId the target channel ID.
+ * @param fetchedPrograms the newly fetched program data.
+ * @return {@code true} if new program data are successfully updated. Otherwise {@code false}.
+ */
+ static boolean updateEpgData(Context context, long channelId, List<Program> fetchedPrograms) {
+ final int fetchedProgramsCount = fetchedPrograms.size();
+ if (fetchedProgramsCount == 0) {
+ return false;
+ }
+ boolean updated = false;
+ long startTimeMs = System.currentTimeMillis();
+ long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION_MS;
+ List<Program> oldPrograms = queryPrograms(context, channelId, startTimeMs, endTimeMs);
+ int oldProgramsIndex = 0;
+ int newProgramsIndex = 0;
+
+ // Compare the new programs with old programs one by one and update/delete the old one
+ // or insert new program if there is no matching program in the database.
+ ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ while (newProgramsIndex < fetchedProgramsCount) {
+ Program oldProgram = oldProgramsIndex < oldPrograms.size()
+ ? oldPrograms.get(oldProgramsIndex) : null;
+ Program newProgram = fetchedPrograms.get(newProgramsIndex);
+ boolean addNewProgram = false;
+ if (oldProgram != null) {
+ if (oldProgram.equals(newProgram)) {
+ // Exact match. No need to update. Move on to the next programs.
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (hasSameTitleAndOverlap(oldProgram, newProgram)) {
+ // Partial match. Update the old program with the new one.
+ // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There
+ // could be application specific settings which belong to the old program.
+ ops.add(ContentProviderOperation.newUpdate(
+ TvContract.buildProgramUri(oldProgram.getId()))
+ .withValues(Program.toContentValues(newProgram))
+ .build());
+ oldProgramsIndex++;
+ newProgramsIndex++;
+ } else if (oldProgram.getEndTimeUtcMillis() < newProgram.getEndTimeUtcMillis()) {
+ // No match. Remove the old program first to see if the next program in
+ // {@code oldPrograms} partially matches the new program.
+ ops.add(ContentProviderOperation.newDelete(
+ TvContract.buildProgramUri(oldProgram.getId()))
+ .build());
+ oldProgramsIndex++;
+ } else {
+ // No match. The new program does not match any of the old programs. Insert
+ // it as a new program.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ } else {
+ // No old programs. Just insert new programs.
+ addNewProgram = true;
+ newProgramsIndex++;
+ }
+ if (addNewProgram) {
+ ops.add(ContentProviderOperation
+ .newInsert(Programs.CONTENT_URI)
+ .withValues(Program.toContentValues(newProgram))
+ .build());
+ }
+ // Throttle the batch operation not to cause TransactionTooLargeException.
+ if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
+ try {
+ if (DEBUG) {
+ int size = ops.size();
+ Log.d(TAG, "Running " + size + " operations for channel " + channelId);
+ for (int i = 0; i < size; ++i) {
+ Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
+ }
+ }
+ context.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
+ updated = true;
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.e(TAG, "Failed to insert programs.", e);
+ return updated;
+ }
+ ops.clear();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
+ }
+ return updated;
+ }
+
+ private static List<Program> queryPrograms(Context context, long channelId,
+ long startTimeMs, long endTimeMs) {
+ try (Cursor c = context.getContentResolver().query(
+ TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
+ Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
+ if (c == null) {
+ return Collections.emptyList();
+ }
+ ArrayList<Program> programs = new ArrayList<>();
+ while (c.moveToNext()) {
+ programs.add(Program.fromCursor(c));
+ }
+ return programs;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the {@code oldProgram} needs to be updated with the
+ * {@code newProgram}.
+ */
+ private static boolean hasSameTitleAndOverlap(Program oldProgram, Program newProgram) {
+ // NOTE: Here, we update the old program if it has the same title and overlaps with the
+ // new program. The test logic is just an example and you can modify this. E.g. check
+ // whether the both programs have the same program ID if your EPG supports any ID for
+ // the programs.
+ return TextUtils.equals(oldProgram.getTitle(), newProgram.getTitle())
+ && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
+ && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
+ }
+
+ /**
+ * Sets the last known lineup ID into shared preferences for future usage. If channels are not
+ * re-scanned, EPG fetcher can directly use this value instead of checking the correct lineup ID
+ * every time when it needs to fetch EPG data.
+ */
+ @WorkerThread
+ synchronized static void setLastLineupId(Context context, String lineupId) {
+ if (DEBUG) {
+ if (lineupId == null) {
+ Log.d(TAG, "Clear stored lineup id: " + sLastLineupId);
+ }
+ }
+ sLastLineupId = lineupId;
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putString(KEY_LAST_LINEUP_ID, lineupId).apply();
+ }
+
+ /**
+ * Gets the last known lineup ID from shared preferences.
+ */
+ synchronized static String getLastLineupId(Context context) {
+ if (sLastLineupId == null) {
+ sLastLineupId = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString(KEY_LAST_LINEUP_ID, null);
+ }
+ if (DEBUG) Log.d(TAG, "Last lineup is " + sLastLineupId);
+ return sLastLineupId;
+ }
+
+ /**
+ * Sets the last updated timestamp of EPG data into shared preferences. If the EPG data is not
+ * out-dated, it's not necessary for EPG fetcher to fetch EPG again.
+ */
+ @WorkerThread
+ synchronized static void setLastEpgUpdatedTimestamp(Context context, long timestamp) {
+ sLastEpgUpdatedTimestamp = timestamp;
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putLong(
+ KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).apply();
+ }
+
+ /**
+ * Gets the last updated timestamp of EPG data.
+ */
+ synchronized static long getLastEpgUpdatedTimestamp(Context context) {
+ if (sLastEpgUpdatedTimestamp < 0) {
+ sLastEpgUpdatedTimestamp = PreferenceManager.getDefaultSharedPreferences(context)
+ .getLong(KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ }
+ return sLastEpgUpdatedTimestamp;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java
index 3b093b6a..24f8b826 100644
--- a/src/com/android/tv/data/epg/EpgFetcher.java
+++ b/src/com/android/tv/data/epg/EpgFetcher.java
@@ -16,570 +16,720 @@
package com.android.tv.data.epg;
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.content.ContentProviderOperation;
-import android.content.ContentValues;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
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;
-import android.media.tv.TvContract;
-import android.media.tv.TvContract.Programs;
-import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
+import android.net.TrafficStats;
+import android.os.AsyncTask;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
-import android.os.RemoteException;
-import android.preference.PreferenceManager;
+import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.Features;
import com.android.tv.TvApplication;
-import com.android.tv.common.WeakHandler;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
+import com.android.tv.config.RemoteConfigUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.InternalDataUtils;
+import com.android.tv.data.ChannelLogoFetcher;
import com.android.tv.data.Lineup;
import com.android.tv.data.Program;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
+import com.android.tv.tuner.util.PostalCodeUtils;
import com.android.tv.util.LocationUtils;
-import com.android.tv.util.RecurringRunner;
+import com.android.tv.util.NetworkTrafficTags;
import com.android.tv.util.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
- * An utility class to fetch the EPG. This class isn't thread-safe.
+ * The service class to fetch EPG routinely or on-demand during channel scanning
+ *
+ * <p>Since the default executor of {@link AsyncTask} is {@link AsyncTask#SERIAL_EXECUTOR}, only one
+ * task can run at a time. Because fetching EPG takes long time, the fetching task shouldn't run on
+ * the serial executor. Instead, it should run on the {@link AsyncTask#THREAD_POOL_EXECUTOR}.
*/
public class EpgFetcher {
private static final String TAG = "EpgFetcher";
private static final boolean DEBUG = false;
- private static final int MSG_FETCH_EPG = 1;
+ private static final int EPG_ROUTINELY_FETCHING_JOB_ID = 101;
+
+ private static final long INITIAL_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);
+
+ private static final int REASON_EPG_READER_NOT_READY = 1;
+ private static final int REASON_LOCATION_INFO_UNAVAILABLE = 2;
+ private static final int REASON_LOCATION_PERMISSION_NOT_GRANTED = 3;
+ private static final int REASON_NO_EPG_DATA_RETURNED = 4;
+ private static final int REASON_NO_NEW_EPG = 5;
- 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 PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30);
+ private static final long FETCH_DURING_SCAN_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(10);
- private static final int BATCH_OPERATION_COUNT = 100;
+ private static final long FETCH_DURING_SCAN_DURATION_SEC = TimeUnit.HOURS.toSeconds(3);
+ private static final long FAST_FETCH_DURATION_SEC = TimeUnit.DAYS.toSeconds(2);
- private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
- private static final String CONTENT_RATING_SEPARATOR = ",";
+ private static final int DEFAULT_ROUTINE_INTERVAL_HOUR = 4;
+ private static final String KEY_ROUTINE_INTERVAL = "live_channels_epg_fetcher_interval_hour";
- // Value: Long
- private static final String KEY_LAST_UPDATED_EPG_TIMESTAMP =
- "com.android.tv.data.epg.EpgFetcher.LastUpdatedEpgTimestamp";
- // Value: String
- private static final String KEY_LAST_LINEUP_ID =
- "com.android.tv.data.epg.EpgFetcher.LastLineupId";
+ private static final int MSG_PREPARE_FETCH_DURING_SCAN = 1;
+ private static final int MSG_CHANNEL_UPDATED_DURING_SCAN = 2;
+ private static final int MSG_FINISH_FETCH_DURING_SCAN = 3;
+ private static final int MSG_RETRY_PREPARE_FETCH_DURING_SCAN = 4;
+
+ private static final int QUERY_CHANNEL_COUNT = 50;
+ private static final int MINIMUM_CHANNELS_TO_DECIDE_LINEUP = 3;
private static EpgFetcher sInstance;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
private final EpgReader mEpgReader;
- private EpgFetcherHandler mHandler;
- private RecurringRunner mRecurringRunner;
- private boolean mStarted;
-
- private long mLastEpgTimestamp = -1;
- private String mLineupId;
-
- public static synchronized EpgFetcher getInstance(Context context) {
+ private final PerformanceMonitor mPerformanceMonitor;
+ private FetchAsyncTask mFetchTask;
+ private FetchDuringScanHandler mFetchDuringScanHandler;
+ private long mEpgTimeStamp;
+ private List<Lineup> mPossibleLineups;
+ private final Object mPossibleLineupsLock = new Object();
+ private final Object mFetchDuringScanHandlerLock = new Object();
+ // A flag to block the re-entrance of onChannelScanStarted and onChannelScanFinished.
+ private boolean mScanStarted;
+
+ private final long mRoutineIntervalMs;
+ private final long mEpgDataExpiredTimeLimitMs;
+ private final long mFastFetchDurationSec;
+
+ public static EpgFetcher getInstance(Context context) {
if (sInstance == null) {
- sInstance = new EpgFetcher(context.getApplicationContext());
+ sInstance = new EpgFetcher(context);
}
return sInstance;
}
- /**
- * Creates and returns {@link EpgReader}.
- */
- public static EpgReader createEpgReader(Context context) {
+ /** Creates and returns {@link EpgReader}. */
+ public static EpgReader createEpgReader(Context context, String region) {
return new StubEpgReader(context);
}
private EpgFetcher(Context context) {
- mContext = context;
- mEpgReader = new StubEpgReader(mContext);
- mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
- mChannelDataManager.addListener(new ChannelDataManager.Listener() {
- @Override
- public void onLoadFinished() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
- handleChannelChanged();
+ mContext = context.getApplicationContext();
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext);
+ mChannelDataManager = applicationSingletons.getChannelDataManager();
+ mPerformanceMonitor = applicationSingletons.getPerformanceMonitor();
+ mEpgReader = createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext));
+
+ int remoteInteval =
+ (int) RemoteConfigUtils.getRemoteConfig(
+ context, KEY_ROUTINE_INTERVAL, DEFAULT_ROUTINE_INTERVAL_HOUR);
+ mRoutineIntervalMs =
+ remoteInteval < 0
+ ? TimeUnit.HOURS.toMillis(DEFAULT_ROUTINE_INTERVAL_HOUR)
+ : TimeUnit.HOURS.toMillis(remoteInteval);
+ mEpgDataExpiredTimeLimitMs = mRoutineIntervalMs * 2;
+ mFastFetchDurationSec = FAST_FETCH_DURATION_SEC + mRoutineIntervalMs / 1000;
+ }
+
+ /**
+ * Starts the routine service of EPG fetching. It use {@link JobScheduler} to schedule the EPG
+ * fetching routine. The EPG fetching routine will be started roughly every 4 hours, unless
+ * the channel scanning of tuner input is started.
+ */
+ @MainThread
+ public void startRoutineService() {
+ JobScheduler jobScheduler =
+ (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ for (JobInfo job : jobScheduler.getAllPendingJobs()) {
+ if (job.getId() == EPG_ROUTINELY_FETCHING_JOB_ID) {
+ return;
}
+ }
+ JobInfo job =
+ new JobInfo.Builder(
+ EPG_ROUTINELY_FETCHING_JOB_ID,
+ new ComponentName(mContext, EpgFetchService.class))
+ .setPeriodic(mRoutineIntervalMs)
+ .setBackoffCriteria(INITIAL_BACKOFF_MS, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
+ .setPersisted(true)
+ .build();
+ jobScheduler.schedule(job);
+ Log.i(TAG, "EPG fetching routine service started.");
+ }
+ /**
+ * Fetches EPG immediately if current EPG data are out-dated, i.e., not successfully updated
+ * by routine fetching service due to various reasons.
+ */
+ @MainThread
+ public void fetchImmediatelyIfNeeded() {
+ if (TvCommonUtils.isRunningInTest()) {
+ // Do not run EpgFetcher in test.
+ return;
+ }
+ new AsyncTask<Void, Void, Long>() {
@Override
- public void onChannelListUpdated() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
- handleChannelChanged();
+ protected Long doInBackground(Void... args) {
+ return EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext);
}
@Override
- public void onChannelBrowsableChanged() {
- if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelBrowsableChanged()");
- handleChannelChanged();
+ protected void onPostExecute(Long result) {
+ if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ Log.i(TAG, "EPG data expired. Start fetching immediately.");
+ fetchImmediately();
+ }
}
- });
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
- private void handleChannelChanged() {
- if (mStarted) {
- if (needToStop()) {
- stop();
- }
- } else {
- start();
- }
- }
+ /**
+ * Fetches EPG immediately.
+ */
+ @MainThread
+ public void fetchImmediately() {
+ if (!mChannelDataManager.isDbLoadFinished()) {
+ mChannelDataManager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ executeFetchTaskIfPossible(null, null);
+ }
- private boolean needToStop() {
- return !canStart();
- }
+ @Override
+ public void onChannelListUpdated() { }
- private boolean canStart() {
- if (DEBUG) Log.d(TAG, "canStart()");
- boolean hasInternalTunerChannel = false;
- for (TvInputInfo input : TvApplication.getSingletons(mContext).getTvInputManagerHelper()
- .getTvInputInfos(true, true)) {
- String inputId = input.getId();
- if (Utils.isInternalTvInput(mContext, inputId)
- && mChannelDataManager.getChannelCountForInput(inputId) > 0) {
- hasInternalTunerChannel = true;
- break;
- }
- }
- if (!hasInternalTunerChannel) {
- if (DEBUG) Log.d(TAG, "No internal tuner channels.");
- return false;
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ } else {
+ executeFetchTaskIfPossible(null, null);
}
+ }
- 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;
+ /**
+ * Notifies EPG fetch service that channel scanning is started.
+ */
+ @MainThread
+ public void onChannelScanStarted() {
+ if (mScanStarted || !Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ return;
}
-
- 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());
- return false;
+ mScanStarted = true;
+ stopFetchingJob();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (mFetchDuringScanHandler == null) {
+ HandlerThread thread = new HandlerThread("EpgFetchDuringScan");
+ thread.start();
+ mFetchDuringScanHandler = new FetchDuringScanHandler(thread.getLooper());
}
- } catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location", e);
- return false;
- } catch (IOException e) {
- Log.w(TAG, "IO Exception when getting the current location", e);
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_PREPARE_FETCH_DURING_SCAN);
}
- return true;
+ Log.i(TAG, "EPG fetching on channel scanning started.");
}
/**
- * Starts fetching EPG.
+ * Notifies EPG fetch service that channel scanning is finished.
*/
@MainThread
- public void start() {
- if (DEBUG) Log.d(TAG, "start()");
- if (mStarted) {
- if (DEBUG) Log.d(TAG, "EpgFetcher thread already started.");
+ public void onChannelScanFinished() {
+ if (!mScanStarted) {
return;
}
- if (!canStart()) {
- return;
+ mScanStarted = false;
+ mFetchDuringScanHandler.sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ }
+
+ @MainThread
+ private void stopFetchingJob() {
+ if (DEBUG) Log.d(TAG, "Try to stop routinely fetching job...");
+ if (mFetchTask != null) {
+ mFetchTask.cancel(true);
+ mFetchTask = null;
+ Log.i(TAG, "EPG routinely fetching job stopped.");
}
- mStarted = true;
- if (DEBUG) Log.d(TAG, "Starting EpgFetcher thread.");
- HandlerThread handlerThread = new HandlerThread("EpgFetcher");
- handlerThread.start();
- mHandler = new EpgFetcherHandler(handlerThread.getLooper(), this);
- mRecurringRunner = new RecurringRunner(mContext, EPG_PREFETCH_RECURRING_PERIOD_MS,
- new EpgRunner(), null);
- mRecurringRunner.start();
- if (DEBUG) Log.d(TAG, "EpgFetcher thread started successfully.");
}
- /**
- * Starts fetching EPG immediately if possible without waiting for the timer.
- */
@MainThread
- public void startImmediately() {
- start();
- if (mStarted) {
- if (DEBUG) Log.d(TAG, "Starting fetcher immediately");
- fetchEpg();
+ private boolean executeFetchTaskIfPossible(JobService service, JobParameters params) {
+ SoftPreconditions.checkState(mChannelDataManager.isDbLoadFinished());
+ if (!TvCommonUtils.isRunningInTest() && checkFetchPrerequisite()) {
+ mFetchTask = new FetchAsyncTask(service, params);
+ mFetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ return true;
}
+ return false;
}
- /**
- * Stops fetching EPG.
- */
@MainThread
- public void stop() {
- if (DEBUG) Log.d(TAG, "stop()");
- if (!mStarted) {
- return;
+ private boolean checkFetchPrerequisite() {
+ if (DEBUG) Log.d(TAG, "Check prerequisite of routinely fetching job.");
+ if (!Features.ENABLE_CLOUD_EPG_REGION.isEnabled(mContext)) {
+ Log.i(TAG, "Cannot start routine service: country not supported: "
+ + LocationUtils.getCurrentCountry(mContext));
+ return false;
+ }
+ if (mFetchTask != null) {
+ // Fetching job is already running or ready to run, no need to start again.
+ return false;
+ }
+ if (mFetchDuringScanHandler != null) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: scanning channels.");
+ return false;
+ }
+ if (getTunerChannelCount() == 0) {
+ if (DEBUG) Log.d(TAG, "Cannot start routine service: no internal tuner channels.");
+ return false;
}
- mStarted = false;
- mRecurringRunner.stop();
- mHandler.removeCallbacksAndMessages(null);
- mHandler.getLooper().quit();
+ if (!TextUtils.isEmpty(EpgFetchHelper.getLastLineupId(mContext))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return true;
+ }
+ return true;
}
- private void fetchEpg() {
- fetchEpg(0);
+ @MainThread
+ private int getTunerChannelCount() {
+ for (TvInputInfo input : TvApplication.getSingletons(mContext)
+ .getTvInputManagerHelper().getTvInputInfos(true, true)) {
+ String inputId = input.getId();
+ if (Utils.isInternalTvInput(mContext, inputId)) {
+ return mChannelDataManager.getChannelCountForInput(inputId);
+ }
+ }
+ return 0;
}
- private void fetchEpg(long delay) {
- mHandler.removeMessages(MSG_FETCH_EPG);
- mHandler.sendEmptyMessageDelayed(MSG_FETCH_EPG, delay);
+ @AnyThread
+ private void clearUnusedLineups(@Nullable String lineupId) {
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return;
+ }
+ for (Lineup lineup : mPossibleLineups) {
+ if (!TextUtils.equals(lineupId, lineup.id)) {
+ mEpgReader.clearCachedChannels(lineup.id);
+ }
+ }
+ mPossibleLineups = null;
+ }
}
- private void onFetchEpg() {
- if (DEBUG) Log.d(TAG, "Start fetching EPG.");
+ @WorkerThread
+ private Integer prepareFetchEpg(boolean forceUpdatePossibleLineups) {
if (!mEpgReader.isAvailable()) {
- if (DEBUG) Log.d(TAG, "EPG reader is not temporarily available.");
- fetchEpg(EPG_READER_INIT_WAIT_MS);
- return;
+ Log.i(TAG, "EPG reader is temporarily unavailable.");
+ return REASON_EPG_READER_NOT_READY;
}
- String lineupId = getLastLineupId();
- if (lineupId == null) {
- Address address;
- try {
- address = LocationUtils.getCurrentAddress(mContext);
- } catch (IOException e) {
- if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
- fetchEpg(LOCATION_ERROR_WAIT_MS);
- return;
- } catch (SecurityException e) {
- Log.w(TAG, "No permission to get the current location.");
- return;
+ // Checks the EPG Timestamp.
+ mEpgTimeStamp = mEpgReader.getEpgTimestamp();
+ if (mEpgTimeStamp <= EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)) {
+ if (DEBUG) Log.d(TAG, "No new EPG.");
+ return REASON_NO_NEW_EPG;
+ }
+ // Updates postal code.
+ boolean postalCodeChanged = false;
+ try {
+ postalCodeChanged = PostalCodeUtils.updatePostalCode(mContext);
+ } catch (IOException e) {
+ if (DEBUG) Log.d(TAG, "Couldn't get the current location.", e);
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return REASON_LOCATION_INFO_UNAVAILABLE;
}
- if (address == null) {
- if (DEBUG) Log.d(TAG, "Null address returned.");
- fetchEpg(LOCATION_INIT_WAIT_MS);
- return;
+ } catch (SecurityException e) {
+ Log.w(TAG, "No permission to get the current location.");
+ if (TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(mContext))) {
+ return REASON_LOCATION_PERMISSION_NOT_GRANTED;
}
- if (DEBUG) Log.d(TAG, "Current location is " + address);
-
- lineupId = getLineupForAddress(address);
- if (lineupId != null) {
- if (DEBUG) Log.d(TAG, "Saving lineup " + lineupId + "found for " + address);
- setLastLineupId(lineupId);
- } else {
- if (DEBUG) Log.d(TAG, "No lineup found for " + address);
- return;
+ } catch (PostalCodeUtils.NoPostalCodeException e) {
+ Log.i(TAG, "Cannot get address or postal code.");
+ return REASON_LOCATION_INFO_UNAVAILABLE;
+ }
+ // Updates possible lineups if necessary.
+ SoftPreconditions.checkState(mPossibleLineups == null, TAG, "Possible lineups not reset.");
+ if (postalCodeChanged || forceUpdatePossibleLineups
+ || EpgFetchHelper.getLastLineupId(mContext) == null) {
+ // To prevent main thread being blocked, though theoretically it should not happen.
+ List<Lineup> possibleLineups =
+ mEpgReader.getLineups(PostalCodeUtils.getLastPostalCode(mContext));
+ if (possibleLineups.isEmpty()) {
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ for (Lineup lineup : possibleLineups) {
+ mEpgReader.preloadChannels(lineup.id);
+ }
+ synchronized (mPossibleLineupsLock) {
+ mPossibleLineups = possibleLineups;
}
+ EpgFetchHelper.setLastLineupId(mContext, null);
}
+ return null;
+ }
- // Check the EPG Timestamp.
- long epgTimestamp = mEpgReader.getEpgTimestamp();
- if (epgTimestamp <= getLastUpdatedEpgTimestamp()) {
- if (DEBUG) Log.d(TAG, "No new EPG.");
+ @WorkerThread
+ private void batchFetchEpg(List<Channel> channels, long durationSec) {
+ Log.i(TAG, "Start batch fetching (" + durationSec + ")...." + channels.size());
+ if (channels.size() == 0) {
return;
}
-
- boolean updated = false;
- List<Channel> channels = mEpgReader.getChannels(lineupId);
+ List<Long> queryChannelIds = new ArrayList<>(QUERY_CHANNEL_COUNT);
for (Channel channel : channels) {
- List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channel.getId()));
- Collections.sort(programs);
- if (DEBUG) {
- Log.d(TAG, "Fetched " + programs.size() + " programs for channel " + channel);
- }
- if (updateEpg(channel.getId(), programs)) {
- updated = true;
+ queryChannelIds.add(channel.getId());
+ if (queryChannelIds.size() >= QUERY_CHANNEL_COUNT) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ queryChannelIds.clear();
}
}
-
- final boolean epgUpdated = updated;
- setLastUpdatedEpgTimestamp(epgTimestamp);
- mHandler.removeMessages(MSG_FETCH_EPG);
- if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ if (!queryChannelIds.isEmpty()) {
+ batchUpdateEpg(mEpgReader.getPrograms(queryChannelIds, durationSec));
+ }
}
- @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);
+ @WorkerThread
+ private void batchUpdateEpg(Map<Long, List<Program>> allPrograms) {
+ for (Map.Entry<Long, List<Program>> entry : allPrograms.entrySet()) {
+ List<Program> programs = entry.getValue();
+ if (programs == null) {
+ continue;
}
+ Collections.sort(programs);
+ Log.i(TAG, "Batch fetched " + programs.size() + " programs for channel "
+ + entry.getKey());
+ EpgFetchHelper.updateEpgData(mContext, entry.getKey(), programs);
}
- return lineup;
}
@Nullable
- private String getLineupForPostalCode(String postalCode) {
- List<Lineup> lineups = mEpgReader.getLineups(postalCode);
- 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;
+ @WorkerThread
+ private String pickBestLineupId(List<Channel> currentChannelList) {
+ String maxLineupId = null;
+ synchronized (mPossibleLineupsLock) {
+ if (mPossibleLineups == null) {
+ return null;
+ }
+ int maxCount = 0;
+ for (Lineup lineup : mPossibleLineups) {
+ int count = getMatchedChannelCount(lineup.id, currentChannelList);
+ Log.i(TAG, lineup.name + " (" + lineup.id + ") - " + count + " matches");
+ if (count > maxCount) {
+ maxCount = count;
+ maxLineupId = lineup.id;
+ }
}
}
- return null;
+ return maxLineupId;
}
- private long getLastUpdatedEpgTimestamp() {
- if (mLastEpgTimestamp < 0) {
- mLastEpgTimestamp = PreferenceManager.getDefaultSharedPreferences(mContext).getLong(
- KEY_LAST_UPDATED_EPG_TIMESTAMP, 0);
+ @WorkerThread
+ private int getMatchedChannelCount(String lineupId, List<Channel> currentChannelList) {
+ // Construct a list of display numbers for existing channels.
+ if (currentChannelList.isEmpty()) {
+ if (DEBUG) Log.d(TAG, "No existing channel to compare");
+ return 0;
+ }
+ List<String> numbers = new ArrayList<>(currentChannelList.size());
+ for (Channel channel : currentChannelList) {
+ // We only support channels from internal tuner inputs.
+ if (Utils.isInternalTvInput(mContext, channel.getInputId())) {
+ numbers.add(channel.getDisplayNumber());
+ }
}
- return mLastEpgTimestamp;
+ numbers.retainAll(mEpgReader.getChannelNumbers(lineupId));
+ return numbers.size();
}
- private void setLastUpdatedEpgTimestamp(long timestamp) {
- mLastEpgTimestamp = timestamp;
- PreferenceManager.getDefaultSharedPreferences(mContext).edit().putLong(
- KEY_LAST_UPDATED_EPG_TIMESTAMP, timestamp).commit();
- }
+ public static class EpgFetchService extends JobService {
+ private EpgFetcher mEpgFetcher;
- private String getLastLineupId() {
- if (mLineupId == null) {
- mLineupId = PreferenceManager.getDefaultSharedPreferences(mContext)
- .getString(KEY_LAST_LINEUP_ID, null);
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ TvApplication.setCurrentRunningProcess(this, true);
+ mEpgFetcher = EpgFetcher.getInstance(this);
}
- if (DEBUG) Log.d(TAG, "Last lineup_id " + mLineupId);
- return mLineupId;
- }
- private void setLastLineupId(String lineupId) {
- mLineupId = lineupId;
- PreferenceManager.getDefaultSharedPreferences(mContext).edit()
- .putString(KEY_LAST_LINEUP_ID, lineupId).commit();
- }
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ if (!mEpgFetcher.mChannelDataManager.isDbLoadFinished()) {
+ mEpgFetcher.mChannelDataManager.addListener(new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ mEpgFetcher.mChannelDataManager.removeListener(this);
+ if (!mEpgFetcher.executeFetchTaskIfPossible(EpgFetchService.this, params)) {
+ jobFinished(params, false);
+ }
+ }
- private boolean updateEpg(long channelId, List<Program> newPrograms) {
- final int fetchedProgramsCount = newPrograms.size();
- if (fetchedProgramsCount == 0) {
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ });
+ return true;
+ } else {
+ return mEpgFetcher.executeFetchTaskIfPossible(this, params);
+ }
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ mEpgFetcher.stopFetchingJob();
return false;
}
- boolean updated = false;
- long startTimeMs = System.currentTimeMillis();
- long endTimeMs = startTimeMs + PROGRAM_QUERY_DURATION;
- List<Program> oldPrograms = queryPrograms(channelId, startTimeMs, endTimeMs);
- Program currentOldProgram = oldPrograms.size() > 0 ? oldPrograms.get(0) : null;
- int oldProgramsIndex = 0;
- int newProgramsIndex = 0;
- // Skip the past programs. They will be automatically removed by the system.
- if (currentOldProgram != null) {
- long oldStartTimeUtcMillis = currentOldProgram.getStartTimeUtcMillis();
- for (Program program : newPrograms) {
- if (program.getEndTimeUtcMillis() > oldStartTimeUtcMillis) {
- break;
- }
- newProgramsIndex++;
- }
+ }
+
+ private class FetchAsyncTask extends AsyncTask<Void, Void, Integer> {
+ private final JobService mService;
+ private final JobParameters mParams;
+ private List<Channel> mCurrentChannelList;
+ private TimerEvent mTimerEvent;
+
+ private FetchAsyncTask(JobService service, JobParameters params) {
+ mService = service;
+ mParams = params;
}
- // Compare the new programs with old programs one by one and update/delete the old one
- // or insert new program if there is no matching program in the database.
- ArrayList<ContentProviderOperation> ops = new ArrayList<>();
- while (newProgramsIndex < fetchedProgramsCount) {
- // TODO: Extract to method and make test.
- Program oldProgram = oldProgramsIndex < oldPrograms.size()
- ? oldPrograms.get(oldProgramsIndex) : null;
- Program newProgram = newPrograms.get(newProgramsIndex);
- boolean addNewProgram = false;
- if (oldProgram != null) {
- if (oldProgram.equals(newProgram)) {
- // Exact match. No need to update. Move on to the next programs.
- oldProgramsIndex++;
- newProgramsIndex++;
- } else if (isSameTitleAndOverlap(oldProgram, newProgram)) {
- // Partial match. Update the old program with the new one.
- // NOTE: Use 'update' in this case instead of 'insert' and 'delete'. There
- // could be application specific settings which belong to the old program.
- ops.add(ContentProviderOperation.newUpdate(
- TvContract.buildProgramUri(oldProgram.getId()))
- .withValues(toContentValues(newProgram))
- .build());
- oldProgramsIndex++;
- newProgramsIndex++;
- } else if (oldProgram.getEndTimeUtcMillis()
- < newProgram.getEndTimeUtcMillis()) {
- // No match. Remove the old program first to see if the next program in
- // {@code oldPrograms} partially matches the new program.
- ops.add(ContentProviderOperation.newDelete(
- TvContract.buildProgramUri(oldProgram.getId()))
- .build());
- oldProgramsIndex++;
+
+ @Override
+ protected void onPreExecute() {
+ mTimerEvent = mPerformanceMonitor.startTimer();
+ mCurrentChannelList = mChannelDataManager.getChannelList();
+ }
+
+ @Override
+ protected Integer doInBackground(Void... args) {
+ final int oldTag = TrafficStats.getThreadStatsTag();
+ TrafficStats.setThreadStatsTag(NetworkTrafficTags.EPG_FETCH);
+ try {
+ if (DEBUG) Log.d(TAG, "Start EPG routinely fetching.");
+ Integer failureReason = prepareFetchEpg(false);
+ // InterruptedException might be caught by RPC, we should check it here.
+ if (failureReason != null || this.isCancelled()) {
+ return failureReason;
+ }
+ String lineupId = EpgFetchHelper.getLastLineupId(mContext);
+ lineupId = lineupId == null ? pickBestLineupId(mCurrentChannelList) : lineupId;
+ if (lineupId != null) {
+ Log.i(TAG, "Selecting the lineup " + lineupId);
+ // During normal fetching process, the lineup ID should be confirmed since all
+ // channels are known, clear up possible lineups to save resources.
+ EpgFetchHelper.setLastLineupId(mContext, lineupId);
+ clearUnusedLineups(lineupId);
} else {
- // No match. The new program does not match any of the old programs. Insert
- // it as a new program.
- addNewProgram = true;
- newProgramsIndex++;
+ Log.i(TAG, "Failed to get lineup id");
+ return REASON_NO_EPG_DATA_RETURNED;
}
- } else {
- // No old programs. Just insert new programs.
- addNewProgram = true;
- newProgramsIndex++;
- }
- if (addNewProgram) {
- ops.add(ContentProviderOperation
- .newInsert(TvContract.Programs.CONTENT_URI)
- .withValues(toContentValues(newProgram))
- .build());
- }
- // Throttle the batch operation not to cause TransactionTooLargeException.
- if (ops.size() > BATCH_OPERATION_COUNT || newProgramsIndex >= fetchedProgramsCount) {
- try {
- if (DEBUG) {
- int size = ops.size();
- Log.d(TAG, "Running " + size + " operations for channel " + channelId);
- for (int i = 0; i < size; ++i) {
- Log.d(TAG, "Operation(" + i + "): " + ops.get(i));
- }
+ final List<Channel> channels = mEpgReader.getChannels(lineupId);
+ // InterruptedException might be caught by RPC, we should check it here.
+ if (this.isCancelled()) {
+ return null;
+ }
+ if (channels.isEmpty()) {
+ Log.i(TAG, "Failed to get EPG channels.");
+ return REASON_NO_EPG_DATA_RETURNED;
+ }
+ if (System.currentTimeMillis() - EpgFetchHelper.getLastEpgUpdatedTimestamp(mContext)
+ > mEpgDataExpiredTimeLimitMs) {
+ batchFetchEpg(channels, mFastFetchDurationSec);
+ }
+ new Handler(mContext.getMainLooper())
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ ChannelLogoFetcher.startFetchingChannelLogos(
+ mContext, channels);
+ }
+ });
+ for (Channel channel : channels) {
+ if (this.isCancelled()) {
+ return null;
}
- mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
- updated = true;
- } catch (RemoteException | OperationApplicationException e) {
- Log.e(TAG, "Failed to insert programs.", e);
- return updated;
+ long channelId = channel.getId();
+ List<Program> programs = new ArrayList<>(mEpgReader.getPrograms(channelId));
+ // InterruptedException might be caught by RPC, we should check it here.
+ Collections.sort(programs);
+ Log.i(TAG, "Fetched " + programs.size() + " programs for channel " + channelId);
+ EpgFetchHelper.updateEpgData(mContext, channelId, programs);
}
- ops.clear();
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, mEpgTimeStamp);
+ if (DEBUG) Log.d(TAG, "Fetching EPG is finished.");
+ return null;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
}
}
- if (DEBUG) {
- Log.d(TAG, "Updated " + fetchedProgramsCount + " programs for channel " + channelId);
- }
- return updated;
- }
- private List<Program> queryPrograms(long channelId, long startTimeMs, long endTimeMs) {
- try (Cursor c = mContext.getContentResolver().query(
- TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
- Program.PROJECTION, null, null, Programs.COLUMN_START_TIME_UTC_MILLIS)) {
- if (c == null) {
- return Collections.emptyList();
- }
- ArrayList<Program> programs = new ArrayList<>();
- while (c.moveToNext()) {
- programs.add(Program.fromCursor(c));
+ @Override
+ protected void onPostExecute(Integer failureReason) {
+ mFetchTask = null;
+ if (failureReason == null || failureReason == REASON_LOCATION_PERMISSION_NOT_GRANTED
+ || failureReason == REASON_NO_NEW_EPG) {
+ jobFinished(false);
+ } else {
+ // Applies back-off policy
+ jobFinished(true);
}
- return programs;
+ mPerformanceMonitor.stopTimer(mTimerEvent, EventNames.FETCH_EPG_TASK);
+ mPerformanceMonitor.recordMemory(EventNames.FETCH_EPG_TASK);
}
- }
- /**
- * Returns {@code true} if the {@code oldProgram} program needs to be updated with the
- * {@code newProgram} program.
- */
- private boolean isSameTitleAndOverlap(Program oldProgram, Program newProgram) {
- // NOTE: Here, we update the old program if it has the same title and overlaps with the
- // new program. The test logic is just an example and you can modify this. E.g. check
- // whether the both programs have the same program ID if your EPG supports any ID for
- // the programs.
- return Objects.equals(oldProgram.getTitle(), newProgram.getTitle())
- && oldProgram.getStartTimeUtcMillis() <= newProgram.getEndTimeUtcMillis()
- && newProgram.getStartTimeUtcMillis() <= oldProgram.getEndTimeUtcMillis();
- }
+ @Override
+ protected void onCancelled(Integer failureReason) {
+ clearUnusedLineups(null);
+ jobFinished(false);
+ }
- @SuppressLint("InlinedApi")
- @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());
- if (BuildCompat.isAtLeastN()) {
- putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
- program.getSeasonNumber());
- putValue(values, TvContract.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, 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());
- String[] canonicalGenres = program.getCanonicalGenres();
- if (canonicalGenres != null && canonicalGenres.length > 0) {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE,
- Genres.encode(canonicalGenres));
- } else {
- putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, "");
- }
- TvContentRating[] ratings = program.getContentRatings();
- if (ratings != null && ratings.length > 0) {
- StringBuilder sb = new StringBuilder(ratings[0].flattenToString());
- for (int i = 1; i < ratings.length; ++i) {
- sb.append(CONTENT_RATING_SEPARATOR);
- sb.append(ratings[i].flattenToString());
+ private void jobFinished(boolean reschedule) {
+ if (mService != null && mParams != null) {
+ // Task is executed from JobService, need to report jobFinished.
+ mService.jobFinished(mParams, reschedule);
}
- putValue(values, TvContract.Programs.COLUMN_CONTENT_RATING, sb.toString());
- } else {
- putValue(values, TvContract.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,
- InternalDataUtils.serializeInternalProviderData(program));
- return values;
- }
-
- private static void putValue(ContentValues contentValues, String key, String value) {
- if (TextUtils.isEmpty(value)) {
- contentValues.putNull(key);
- } else {
- contentValues.put(key, value);
}
}
- private static void putValue(ContentValues contentValues, String key, byte[] value) {
- if (value == null || value.length == 0) {
- contentValues.putNull(key);
- } else {
- contentValues.put(key, value);
- }
- }
+ @WorkerThread
+ private class FetchDuringScanHandler extends Handler {
+ private final Set<Long> mFetchedChannelIdsDuringScan = new HashSet<>();
+ private String mPossibleLineupId;
+
+ private final ChannelDataManager.Listener mDuringScanChannelListener =
+ new ChannelDataManager.Listener() {
+ @Override
+ public void onLoadFinished() {
+ if (DEBUG) Log.d(TAG, "ChannelDataManager.onLoadFinished()");
+ if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
+ && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ Message.obtain(FetchDuringScanHandler.this,
+ MSG_CHANNEL_UPDATED_DURING_SCAN, new ArrayList<>(
+ mChannelDataManager.getChannelList())).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onChannelListUpdated() {
+ if (DEBUG) Log.d(TAG, "ChannelDataManager.onChannelListUpdated()");
+ if (getTunerChannelCount() >= MINIMUM_CHANNELS_TO_DECIDE_LINEUP
+ && !hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ Message.obtain(FetchDuringScanHandler.this,
+ MSG_CHANNEL_UPDATED_DURING_SCAN,
+ mChannelDataManager.getChannelList()).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onChannelBrowsableChanged() {
+ // Do nothing
+ }
+ };
- private static class EpgFetcherHandler extends WeakHandler<EpgFetcher> {
- public EpgFetcherHandler (@NonNull Looper looper, EpgFetcher ref) {
- super(looper, ref);
+ @AnyThread
+ private FetchDuringScanHandler(Looper looper) {
+ super(looper);
}
@Override
- public void handleMessage(Message msg, @NonNull EpgFetcher epgFetcher) {
+ public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_FETCH_EPG:
- epgFetcher.onFetchEpg();
+ case MSG_PREPARE_FETCH_DURING_SCAN:
+ case MSG_RETRY_PREPARE_FETCH_DURING_SCAN:
+ onPrepareFetchDuringScan();
break;
- default:
- super.handleMessage(msg);
+ case MSG_CHANNEL_UPDATED_DURING_SCAN:
+ if (!hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ onChannelUpdatedDuringScan((List<Channel>) msg.obj);
+ }
+ break;
+ case MSG_FINISH_FETCH_DURING_SCAN:
+ removeMessages(MSG_RETRY_PREPARE_FETCH_DURING_SCAN);
+ if (hasMessages(MSG_CHANNEL_UPDATED_DURING_SCAN)) {
+ sendEmptyMessage(MSG_FINISH_FETCH_DURING_SCAN);
+ } else {
+ onFinishFetchDuringScan();
+ }
break;
}
}
- }
- private class EpgRunner implements Runnable {
- @Override
- public void run() {
- fetchEpg();
+ private void onPrepareFetchDuringScan() {
+ Integer failureReason = prepareFetchEpg(true);
+ if (failureReason != null) {
+ sendEmptyMessageDelayed(
+ MSG_RETRY_PREPARE_FETCH_DURING_SCAN, FETCH_DURING_SCAN_WAIT_TIME_MS);
+ return;
+ }
+ mChannelDataManager.addListener(mDuringScanChannelListener);
+ }
+
+ private void onChannelUpdatedDuringScan(List<Channel> currentChannelList) {
+ String lineupId = pickBestLineupId(currentChannelList);
+ Log.i(TAG, "Fast fetch channels for lineup ID: " + lineupId);
+ if (TextUtils.isEmpty(lineupId)) {
+ if (TextUtils.isEmpty(mPossibleLineupId)) {
+ return;
+ }
+ } else if (!TextUtils.equals(lineupId, mPossibleLineupId)) {
+ mFetchedChannelIdsDuringScan.clear();
+ mPossibleLineupId = lineupId;
+ }
+ List<Long> currentChannelIds = new ArrayList<>();
+ for (Channel channel : currentChannelList) {
+ currentChannelIds.add(channel.getId());
+ }
+ mFetchedChannelIdsDuringScan.retainAll(currentChannelIds);
+ List<Channel> newChannels = new ArrayList<>();
+ for (Channel channel : mEpgReader.getChannels(mPossibleLineupId)) {
+ if (!mFetchedChannelIdsDuringScan.contains(channel.getId())) {
+ newChannels.add(channel);
+ mFetchedChannelIdsDuringScan.add(channel.getId());
+ }
+ }
+ batchFetchEpg(newChannels, FETCH_DURING_SCAN_DURATION_SEC);
+ }
+
+ private void onFinishFetchDuringScan() {
+ mChannelDataManager.removeListener(mDuringScanChannelListener);
+ EpgFetchHelper.setLastLineupId(mContext, mPossibleLineupId);
+ clearUnusedLineups(null);
+ mFetchedChannelIdsDuringScan.clear();
+ synchronized (mFetchDuringScanHandlerLock) {
+ if (!hasMessages(MSG_PREPARE_FETCH_DURING_SCAN)) {
+ removeCallbacksAndMessages(null);
+ getLooper().quit();
+ mFetchDuringScanHandler = null;
+ }
+ }
+ // Clear timestamp to make routine service start right away.
+ EpgFetchHelper.setLastEpgUpdatedTimestamp(mContext, 0);
+ Log.i(TAG, "EPG Fetching during channel scanning finished.");
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ fetchImmediately();
+ }
+ });
}
}
}
diff --git a/src/com/android/tv/data/epg/EpgReader.java b/src/com/android/tv/data/epg/EpgReader.java
index 4f3b6f52..c5aeca27 100644
--- a/src/com/android/tv/data/epg/EpgReader.java
+++ b/src/com/android/tv/data/epg/EpgReader.java
@@ -16,15 +16,17 @@
package com.android.tv.data.epg;
+import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
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.
@@ -42,25 +44,48 @@ public interface EpgReader {
*/
long getEpgTimestamp();
+ /** Sets the region code. */
+ void setRegionCode(String regionCode);
+
+ /** Returns the lineups list. */
+ List<Lineup> getLineups(@NonNull String postalCode);
+
/**
- * Returns the channels list.
+ * Returns the list of channel numbers (unsorted) for the given lineup. The result is used to
+ * choose the most appropriate lineup among others by comparing the channel numbers of the
+ * existing channels on the device.
+ */
+ List<String> getChannelNumbers(@NonNull String lineupId);
+
+ /**
+ * Returns the list of channels for the given lineup. The returned channels should map into the
+ * existing channels on the device. This method is usually called after selecting the lineup.
*/
List<Channel> getChannels(@NonNull String lineupId);
+ /** Pre-loads and caches channels for a given lineup. */
+ void preloadChannels(@NonNull String lineupId);
+
/**
- * Returns the lineups list.
+ * Clears cached channels for a given lineup.
*/
- List<Lineup> getLineups(@NonNull String postalCode);
+ @AnyThread
+ void clearCachedChannels(@NonNull String lineupId);
/**
- * Returns the programs for the given channel. The result is sorted by the start time.
- * Note that the {@code Program} doesn't have valid program ID because it's not retrieved from
- * TvProvider.
+ * 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<Program> getPrograms(long channelId);
/**
- * Returns the series information for the given series ID.
+ * 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.
*/
- SeriesInfo getSeriesInfo(String seriesId);
-}
+ Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration);
+
+ /** Returns the series information for the given series ID. */
+ SeriesInfo getSeriesInfo(@NonNull String seriesId);
+} \ No newline at end of file
diff --git a/src/com/android/tv/data/epg/StubEpgReader.java b/src/com/android/tv/data/epg/StubEpgReader.java
index 64093f89..ab6935ad 100644
--- a/src/com/android/tv/data/epg/StubEpgReader.java
+++ b/src/com/android/tv/data/epg/StubEpgReader.java
@@ -18,13 +18,15 @@ package com.android.tv.data.epg;
import android.content.Context;
+import android.support.annotation.NonNull;
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,22 +46,47 @@ public class StubEpgReader implements EpgReader{
}
@Override
- public List<Channel> getChannels(String lineupId) {
+ public void setRegionCode(String regionCode) {
+ // Do nothing
+ }
+
+ @Override
+ public List<Lineup> getLineups(@NonNull String postalCode) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<String> getChannelNumbers(@NonNull String lineupId) {
return Collections.emptyList();
}
@Override
- public List<Lineup> getLineups(String postalCode) {
+ public List<Channel> getChannels(@NonNull String lineupId) {
return Collections.emptyList();
}
@Override
+ public void preloadChannels(@NonNull String lineupId) {
+ // Do nothing
+ }
+
+ @Override
+ public void clearCachedChannels(@NonNull String lineupId) {
+ // Do nothing
+ }
+
+ @Override
public List<Program> getPrograms(long channelId) {
return Collections.emptyList();
}
@Override
- public SeriesInfo getSeriesInfo(String seriesId) {
+ public Map<Long, List<Program>> getPrograms(@NonNull List<Long> channelIds, long duration) {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public SeriesInfo getSeriesInfo(@NonNull String seriesId) {
return null;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dialog/DvrHistoryDialogFragment.java b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
new file mode 100644
index 00000000..d686e6e6
--- /dev/null
+++ b/src/com/android/tv/dialog/DvrHistoryDialogFragment.java
@@ -0,0 +1,130 @@
+/*
+ * 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.dvr.ui.DvrUiHelper;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Displays the DVR history.
+ */
+@TargetApi(VERSION_CODES.N)
+public class DvrHistoryDialogFragment extends SafeDismissDialogFragment {
+ public static final String DIALOG_TAG = DvrHistoryDialogFragment.class.getSimpleName();
+
+ private static final String TRACKER_LABEL = "DVR history";
+ private final List<ScheduledRecording> mSchedules = new ArrayList<>();
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
+ DvrDataManager dataManager = singletons.getDvrDataManager();
+ ChannelDataManager channelDataManager = singletons.getChannelDataManager();
+ for (ScheduledRecording schedule : dataManager.getAllScheduledRecordings()) {
+ if (!schedule.isInProgress() && !schedule.isNotStarted()) {
+ mSchedules.add(schedule);
+ }
+ }
+ mSchedules.sort(ScheduledRecording.START_TIME_COMPARATOR.reversed());
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ ArrayAdapter adapter = new ArrayAdapter<ScheduledRecording>(getContext(),
+ R.layout.list_item_dvr_history, ScheduledRecording.toArray(mSchedules)) {
+ @NonNull
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = inflater.inflate(R.layout.list_item_dvr_history, parent, false);
+ ScheduledRecording schedule = mSchedules.get(position);
+ setText(view, R.id.state, getStateString(schedule.getState()));
+ setText(view, R.id.schedule_time, getRecordingTimeText(schedule));
+ setText(view, R.id.program_title, DvrUiHelper.getStyledTitleWithEpisodeNumber(
+ getContext(), schedule, 0));
+ setText(view, R.id.channel_name, getChannelNameText(schedule));
+ return view;
+ }
+
+ private void setText(View view, int id, CharSequence text) {
+ ((TextView) view.findViewById(id)).setText(text);
+ }
+
+ private void setText(View view, int id, int text) {
+ ((TextView) view.findViewById(id)).setText(text);
+ }
+
+ @SuppressLint("SwitchIntDef")
+ private int getStateString(@RecordingState int state) {
+ switch (state) {
+ case ScheduledRecording.STATE_RECORDING_CLIPPED:
+ return R.string.dvr_history_dialog_state_clip;
+ case ScheduledRecording.STATE_RECORDING_FAILED:
+ return R.string.dvr_history_dialog_state_fail;
+ case ScheduledRecording.STATE_RECORDING_FINISHED:
+ return R.string.dvr_history_dialog_state_success;
+ default:
+ break;
+ }
+ return 0;
+ }
+
+ private String getChannelNameText(ScheduledRecording schedule) {
+ Channel channel = channelDataManager.getChannel(schedule.getChannelId());
+ return channel == null ? null :
+ TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() :
+ channel.getDisplayName().trim() + " " + channel.getDisplayNumber();
+ }
+
+ private String getRecordingTimeText(ScheduledRecording schedule) {
+ return Utils.getDurationString(getContext(), schedule.getStartTimeMs(),
+ schedule.getEndTimeMs(), true, true, true, 0);
+ }
+ };
+ ListView listView = new ListView(getActivity());
+ listView.setAdapter(adapter);
+ return new AlertDialog.Builder(getActivity()).setTitle(R.string.dvr_history_dialog_title)
+ .setView(listView).create();
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+}
diff --git a/src/com/android/tv/dialog/FullscreenDialogFragment.java b/src/com/android/tv/dialog/FullscreenDialogFragment.java
index d16202a1..d00422a7 100644
--- a/src/com/android/tv/dialog/FullscreenDialogFragment.java
+++ b/src/com/android/tv/dialog/FullscreenDialogFragment.java
@@ -77,7 +77,7 @@ public class FullscreenDialogFragment extends SafeDismissDialogFragment {
return mTrackerLabel;
}
- private class FullscreenDialog extends TvDialog {
+ private class FullscreenDialog extends Dialog {
public FullscreenDialog(Context context, int theme) {
super(context, theme);
}
diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dialog/HalfSizedDialogFragment.java
index d320816e..315c6a93 100644
--- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java
+++ b/src/com/android/tv/dialog/HalfSizedDialogFragment.java
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dialog;
+import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Handler;
@@ -25,7 +26,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.dialog.SafeDismissDialogFragment;
import java.util.concurrent.TimeUnit;
@@ -54,13 +54,6 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
@Override
public void onStart() {
super.onStart();
- getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
- public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) {
- mHandler.removeCallbacks(mAutoDismisser);
- mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
- return false;
- }
- });
mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
}
@@ -81,6 +74,19 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment {
}
@Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) {
+ mHandler.removeCallbacks(mAutoDismisser);
+ mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS);
+ return false;
+ }
+ });
+ return dialog;
+ }
+
+ @Override
public int getTheme() {
return R.style.Theme_TV_dialog_HalfSizedDialog;
}
diff --git a/src/com/android/tv/dialog/PinDialogFragment.java b/src/com/android/tv/dialog/PinDialogFragment.java
index d9d6c73f..d5c154da 100644
--- a/src/com/android/tv/dialog/PinDialogFragment.java
+++ b/src/com/android/tv/dialog/PinDialogFragment.java
@@ -28,6 +28,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Resources;
+import android.media.tv.TvContentRating;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
@@ -45,11 +46,13 @@ import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.util.TvSettings;
public class PinDialogFragment extends SafeDismissDialogFragment {
private static final String TAG = "PinDialogFragment";
- private static final boolean DBG = true;
+ private static final boolean DEBUG = true;
/**
* PIN code dialog for unlock channel
@@ -80,18 +83,13 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
*/
public static final int PIN_DIALOG_TYPE_UNLOCK_DVR = 5;
- private static final int PIN_DIALOG_RESULT_SUCCESS = 0;
- private static final int PIN_DIALOG_RESULT_FAIL = 1;
-
private static final int MAX_WRONG_PIN_COUNT = 5;
private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute
private static final String INITIAL_TEXT = "—";
private static final String TRACKER_LABEL = "Pin dialog";
-
- public interface ResultListener {
- void done(boolean success);
- }
+ private static final String ARGS_TYPE = "args_type";
+ private static final String ARGS_RATING = "args_rating";
public static final String DIALOG_TAG = PinDialogFragment.class.getName();
@@ -99,8 +97,9 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
R.id.first, R.id.second, R.id.third, R.id.fourth };
private int mType;
- private ResultListener mListener;
- private int mRetCode;
+ private int mRequestType;
+ private boolean mPinChecked;
+ private boolean mDismissSilently;
private TextView mWrongPinView;
private View mEnterPinView;
@@ -114,29 +113,35 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
private long mDisablePinUntil;
private final Handler mHandler = new Handler();
- public PinDialogFragment(int type, ResultListener listener) {
- this(type, listener, null);
+ public static PinDialogFragment create(int type) {
+ return create(type, null);
}
- public PinDialogFragment(int type, ResultListener listener, String rating) {
- mType = type;
- mListener = listener;
- mRetCode = PIN_DIALOG_RESULT_FAIL;
- mRatingString = rating;
+ public static PinDialogFragment create(int type, String rating) {
+ PinDialogFragment fragment = new PinDialogFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARGS_TYPE, type);
+ args.putString(ARGS_RATING, rating);
+ fragment.setArguments(args);
+ return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mRequestType = getArguments().getInt(ARGS_TYPE, PIN_DIALOG_TYPE_ENTER_PIN);
+ mType = mRequestType;
+ mRatingString = getArguments().getString(ARGS_RATING);
setStyle(STYLE_NO_TITLE, 0);
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
mDisablePinUntil = TvSettings.getDisablePinUntil(getActivity());
if (ActivityManager.isUserAMonkey()) {
// Skip PIN dialog half the time for monkeys
if (Math.random() < 0.5) {
- exit(PIN_DIALOG_RESULT_SUCCESS);
+ exit(true);
}
}
+ mPinChecked = false;
}
@Override
@@ -186,7 +191,19 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
mTitleView.setText(R.string.pin_enter_unlock_program);
break;
case PIN_DIALOG_TYPE_UNLOCK_DVR:
- mTitleView.setText(getString(R.string.pin_enter_unlock_dvr, mRatingString));
+ TvContentRating tvContentRating =
+ TvContentRating.unflattenFromString(mRatingString);
+ if (TvContentRating.UNRATED.equals(tvContentRating)) {
+ mTitleView.setText(getString(R.string.pin_enter_unlock_dvr_unrated));
+ } else {
+ mTitleView.setText(
+ getString(
+ R.string.pin_enter_unlock_dvr,
+ TvApplication.getSingletons(getContext())
+ .getTvInputManagerHelper()
+ .getContentRatingsManager()
+ .getDisplayNameForRating(tvContentRating)));
+ }
break;
case PIN_DIALOG_TYPE_ENTER_PIN:
mTitleView.setText(R.string.pin_enter_pin);
@@ -217,10 +234,6 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
return v;
}
- public void setResultListener(ResultListener listener) {
- mListener = listener;
- }
-
private final Runnable mUpdateEnterPinRunnable = new Runnable() {
@Override
public void run() {
@@ -250,18 +263,27 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
}
- private void exit(int retCode) {
- mRetCode = retCode;
+ private void exit(boolean pinChecked) {
+ mPinChecked = pinChecked;
+ dismiss();
+ }
+
+ /** Dismisses the pin dialog without calling activity listener. */
+ public void dismissSilently() {
+ mDismissSilently = true;
dismiss();
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
- if (DBG) Log.d(TAG, "onDismiss: mRetCode=" + mRetCode);
- if (mListener != null) {
- mListener.done(mRetCode == PIN_DIALOG_RESULT_SUCCESS);
+ if (DEBUG) Log.d(TAG, "onDismiss: mPinChecked=" + mPinChecked);
+ SoftPreconditions.checkState(getActivity() instanceof OnPinCheckedListener);
+ if (!mDismissSilently && getActivity() instanceof OnPinCheckedListener) {
+ ((OnPinCheckedListener) getActivity()).onPinChecked(
+ mPinChecked, mRequestType, mRatingString);
}
+ mDismissSilently = false;
}
private void handleWrongPin() {
@@ -279,15 +301,14 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
private void done(String pin) {
- if (DBG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin());
+ if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin + " stored=" + getPin());
switch (mType) {
case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
case PIN_DIALOG_TYPE_UNLOCK_DVR:
case PIN_DIALOG_TYPE_ENTER_PIN:
- // TODO: Implement limited number of retrials and timeout logic.
if (TextUtils.isEmpty(getPin()) || pin.equals(getPin())) {
- exit(PIN_DIALOG_RESULT_SUCCESS);
+ exit(true);
} else {
resetPinInput();
handleWrongPin();
@@ -301,7 +322,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
} else {
if (pin.equals(mPrevPin)) {
setPin(pin);
- exit(PIN_DIALOG_RESULT_SUCCESS);
+ exit(true);
} else {
if (TextUtils.isEmpty(getPin())) {
mTitleView.setText(R.string.pin_enter_create_pin);
@@ -332,7 +353,7 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
}
private void setPin(String pin) {
- if (DBG) Log.d(TAG, "setPin: " + pin);
+ if (DEBUG) Log.d(TAG, "setPin: " + pin);
mPin = pin;
mSharedPreferences.edit().putString(TvSettings.PREF_PIN, pin).apply();
}
@@ -684,4 +705,20 @@ public class PinDialogFragment extends SafeDismissDialogFragment {
: (value > mMaxValue) ? value - interval : value;
}
}
+
+ /**
+ * A listener to the result of {@link PinDialogFragment}. Any activity requiring pin code
+ * checking should implement this listener to receive the result.
+ */
+ public interface OnPinCheckedListener {
+ /**
+ * Called when {@link PinDialogFragment} is dismissed.
+ *
+ * @param checked {@code true} if the pin code entered is checked to be correct,
+ * otherwise {@code false}.
+ * @param type The dialog type regarding to what pin entering is for.
+ * @param rating The target rating to unblock for.
+ */
+ void onPinChecked(boolean checked, int type, String rating);
+ }
}
diff --git a/src/com/android/tv/dialog/SafeDismissDialogFragment.java b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
index f671a87d..e3390b0a 100644
--- a/src/com/android/tv/dialog/SafeDismissDialogFragment.java
+++ b/src/com/android/tv/dialog/SafeDismissDialogFragment.java
@@ -17,11 +17,7 @@
package com.android.tv.dialog;
import android.app.Activity;
-import android.app.Dialog;
import android.app.DialogFragment;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.KeyEvent;
import com.android.tv.MainActivity;
import com.android.tv.TvApplication;
@@ -39,11 +35,6 @@ public abstract class SafeDismissDialogFragment extends DialogFragment
private Tracker mTracker;
@Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new TvDialog(getActivity(), getTheme());
- }
-
- @Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mAttached = true;
@@ -92,21 +83,4 @@ public abstract class SafeDismissDialogFragment extends DialogFragment
super.dismiss();
}
}
-
- protected class TvDialog extends Dialog {
- public TvDialog(Context context, int theme) {
- super(context, theme);
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- // When a dialog is showing, key events are handled by the dialog instead of
- // MainActivity. Therefore, unless a key is a global key, it should be handled here.
- if (mAttached && keyCode == KeyEvent.KEYCODE_SEARCH && mActivity != null) {
- mActivity.showSearchActivity();
- return true;
- }
- return super.onKeyUp(keyCode, event);
- }
- }
}
diff --git a/src/com/android/tv/dialog/WebDialogFragment.java b/src/com/android/tv/dialog/WebDialogFragment.java
index 75f93bb2..171a256b 100644
--- a/src/com/android/tv/dialog/WebDialogFragment.java
+++ b/src/com/android/tv/dialog/WebDialogFragment.java
@@ -37,6 +37,7 @@ public class WebDialogFragment extends SafeDismissDialogFragment {
private static final String TITLE = "TITLE";
private static final String TRACKER_LABEL = "TRACKER_LABEL";
+ private WebView mWebView;
private String mTrackerLabel;
/**
@@ -73,13 +74,21 @@ public class WebDialogFragment extends SafeDismissDialogFragment {
String title = getArguments().getString(TITLE);
getDialog().setTitle(title);
- WebView webView = new WebView(getActivity());
- webView.setWebViewClient(new WebViewClient());
+ mWebView = new WebView(getActivity());
+ mWebView.setWebViewClient(new WebViewClient());
String url = getArguments().getString(URL);
- webView.loadUrl(url);
+ mWebView.loadUrl(url);
Log.d(TAG, "Loading web content from " + url);
- return webView;
+ return mWebView;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mWebView != null) {
+ mWebView.destroy();
+ }
}
@Override
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 89661df3..a8637449 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -26,7 +26,10 @@ import android.util.Log;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.Clock;
import java.util.ArrayList;
@@ -318,5 +321,41 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) {
+ List<SeriesRecording> toRemove = new ArrayList<>();
+ for (long rId : seriesRecordingIds) {
+ SeriesRecording seriesRecording = getSeriesRecording(rId);
+ if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) {
+ toRemove.add(seriesRecording);
+ }
+ }
+ removeSeriesRecording(SeriesRecording.toArray(toRemove));
+ }
+
+ /**
+ * Returns {@code true}, if the series recording is empty and can be removed. If a series
+ * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be
+ * removed.
+ */
+ protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) {
+ if (!seriesRecording.isStopped()) {
+ return false;
+ }
+ long seriesRecordingId = seriesRecording.getId();
+ for (ScheduledRecording r : getAvailableScheduledRecordings()) {
+ if (r.getSeriesRecordingId() == seriesRecordingId) {
+ return false;
+ }
+ }
+ String seriesId = seriesRecording.getSeriesId();
+ for (RecordedProgram r : getRecordedPrograms()) {
+ if (seriesId.equals(r.getSeriesId())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
public void forgetStorage(String inputId) { }
}
diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java
index 06613667..6d400b82 100644
--- a/src/com/android/tv/dvr/DvrDataManager.java
+++ b/src/com/android/tv/dvr/DvrDataManager.java
@@ -21,7 +21,10 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Range;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.Collection;
import java.util.List;
@@ -211,6 +214,13 @@ public interface DvrDataManager {
Collection<Long> getDisallowedProgramIds();
/**
+ * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains
+ * any available schedules or recorded programs, and it's status is
+ * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings.
+ */
+ void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds);
+
+ /**
* Listens for the DVR schedules loading finished.
*/
interface OnDvrScheduleLoadFinishedListener {
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index 46682a48..6094ca72 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,12 +55,14 @@ 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;
import com.android.tv.util.Filter;
import com.android.tv.util.TvInputManagerHelper;
-import com.android.tv.util.TvProviderUriMatcher;
+import com.android.tv.util.TvUriMatcher;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -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,8 +315,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (uri == null) {
uri = RecordedPrograms.CONTENT_URI;
}
- int match = TvProviderUriMatcher.match(uri);
- if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) {
+ if (recordedPrograms == null) {
+ recordedPrograms = Collections.emptyList();
+ }
+ int match = TvUriMatcher.match(uri);
+ if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM) {
if (!mRecordedProgramLoadFinished) {
for (RecordedProgram recorded : recordedPrograms) {
if (isInputAvailable(recorded.getInputId())) {
@@ -318,7 +330,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
mRecordedProgramLoadFinished = true;
notifyRecordedProgramLoadFinished();
- } else if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ if (isInitialized()) {
+ mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
+ mDbSync.start();
+ }
+ } else if (recordedPrograms.isEmpty()) {
List<RecordedProgram> oldRecordedPrograms =
new ArrayList<>(mRecordedPrograms.values());
mRecordedPrograms.clear();
@@ -355,19 +371,24 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (isInitialized()) {
+ validateSeriesRecordings();
SeriesRecordingScheduler.getInstance(mContext).start();
}
- } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
+ } else if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
if (!mRecordedProgramLoadFinished) {
return;
}
long id = ContentUris.parseId(uri);
if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
- if (recordedPrograms == null || recordedPrograms.isEmpty()) {
+ if (recordedPrograms.isEmpty()) {
mRecordedProgramsForRemovedInput.remove(id);
RecordedProgram old = mRecordedPrograms.remove(id);
if (old != null) {
notifyRecordedProgramsRemoved(old);
+ SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId());
+ if (r != null && isEmptySeriesRecording(r)) {
+ removeSeriesRecording(r);
+ }
}
} else {
RecordedProgram recordedProgram = recordedPrograms.get(0);
@@ -592,10 +613,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>();
+ Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
mScheduledRecordings.remove(r.getId());
- getDeletedScheduleMap().remove(r.getId());
+ getDeletedScheduleMap().remove(r.getProgramId());
mProgramId2ScheduledRecordings.remove(r.getProgramId());
+ if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
+ && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
+ }
boolean isScheduleForRemovedInput =
mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null;
// If it belongs to the series recording and it's not started yet, just mark delete
@@ -614,8 +641,19 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
if (mDvrLoadFinished) {
+ if (mRecordedProgramLoadFinished) {
+ checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
+ }
notifyScheduledRecordingRemoved(schedules);
}
+ Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator();
+ while (iterator.hasNext()) {
+ ScheduledRecording r = iterator.next();
+ if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) {
+ iterator.remove();
+ schedulesToDelete.add(r);
+ }
+ }
if (!schedulesToDelete.isEmpty()) {
new AsyncDeleteScheduleTask(mContext).executeOnDbThread(
ScheduledRecording.toArray(schedulesToDelete));
@@ -669,6 +707,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) {
List<ScheduledRecording> toUpdate = new ArrayList<>();
+ Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG,
"Recording not found for: " + r)) {
@@ -691,6 +730,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (programId != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(programId, r);
}
+ if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
+ && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
+ // If the scheduled recording is failed, it may cause the automatically generated
+ // series recording for this schedule becomes invalid (with no future schedules and
+ // past recordings.) We should check and remove these series recordings.
+ seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
+ }
}
if (toUpdate.isEmpty()) {
return;
@@ -702,12 +748,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (updateDb) {
new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
}
+ checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
removeDeletedSchedules(schedules);
}
@Override
public void updateSeriesRecording(final SeriesRecording... seriesRecordings) {
for (SeriesRecording r : seriesRecordings) {
+ if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG,
+ "Non Existing Series ID: " + r)) {
+ continue;
+ }
SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r);
SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be"
@@ -769,14 +820,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return r.getInputId().equals(inputId);
}
});
- List<SeriesRecording> movedSeriesRecordings =
- moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
List<RecordedProgram> movedRecordedPrograms =
moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms,
new Filter<RecordedProgram>() {
@@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
return r.getInputId().equals(inputId);
}
});
+ List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
+ List<SeriesRecording> movedSeriesRecordings =
+ moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings,
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ if (r.getInputId().equals(inputId)) {
+ if (!isEmptySeriesRecording(r)) {
+ return true;
+ }
+ removedSeriesRecordings.add(r);
+ }
+ return false;
+ }
+ });
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule);
@@ -795,6 +853,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording);
}
}
+ for (SeriesRecording r : removedSeriesRecordings) {
+ mSeriesRecordingsForRemovedInput.remove(r.getId());
+ }
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(
+ SeriesRecording.toArray(removedSeriesRecordings));
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
@@ -811,20 +874,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
if (DEBUG) Log.d(TAG, "hideInput " + inputId);
List<ScheduledRecording> movedSchedules =
moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput,
- new Filter<ScheduledRecording>() {
- @Override
- public boolean filter(ScheduledRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ new Filter<ScheduledRecording>() {
+ @Override
+ public boolean filter(ScheduledRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
List<SeriesRecording> movedSeriesRecordings =
moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput,
- new Filter<SeriesRecording>() {
- @Override
- public boolean filter(SeriesRecording r) {
- return r.getInputId().equals(inputId);
- }
- });
+ new Filter<SeriesRecording>() {
+ @Override
+ public boolean filter(SeriesRecording r) {
+ return r.getInputId().equals(inputId);
+ }
+ });
List<RecordedProgram> movedRecordedPrograms =
moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput,
new Filter<RecordedProgram>() {
@@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}
}
+ private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) {
+ int i = 0;
+ long[] rIds = new long[seriesRecordingIds.size()];
+ for (long rId : seriesRecordingIds) {
+ rIds[i++] = rId;
+ }
+ checkAndRemoveEmptySeriesRecording(rIds);
+ }
+
@Override
public void forgetStorage(String inputId) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
@@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
}.executeOnDbThread();
}
+ private void validateSeriesRecordings() {
+ Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator();
+ List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
+ while (iter.hasNext()) {
+ SeriesRecording r = iter.next();
+ if (isEmptySeriesRecording(r)) {
+ iter.remove();
+ removedSeriesRecordings.add(r);
+ }
+ }
+ if (!removedSeriesRecordings.isEmpty()) {
+ SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
+ new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
+ if (mDvrLoadFinished) {
+ notifySeriesRecordingRemoved(removed);
+ }
+ }
+ }
+
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java
index 5fa6f90f..d222003d 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;
@@ -142,7 +144,7 @@ public class DvrManager {
}
private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) {
- if (recordedProgram.getSeriesId() != null) {
+ if (recordedProgram.isEpisodic()) {
SeriesRecording seriesRecording =
mDataManager.getSeriesRecording(recordedProgram.getSeriesId());
if (seriesRecording == null) {
@@ -234,7 +236,7 @@ public class DvrManager {
* Adds a new series recording and schedules for the programs with the initial state.
*/
public SeriesRecording addSeriesRecording(Program selectedProgram,
- List<Program> programsToSchedule, @SeriesState int initialState) {
+ List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) {
Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: "
+ programsToSchedule);
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
@@ -308,8 +310,7 @@ public class DvrManager {
ScheduledRecording scheduleWithSameProgram =
mDataManager.getScheduledRecordingForProgramId(program.getId());
if (scheduleWithSameProgram != null) {
- if (scheduleWithSameProgram.getState()
- == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
+ if (scheduleWithSameProgram.isNotStarted()) {
ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram)
.setSeriesRecordingId(series.getId())
.build();
@@ -337,10 +338,10 @@ public class DvrManager {
*/
public void updateSeriesRecording(SeriesRecording series) {
if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
- SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext);
- scheduler.pauseUpdate();
SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
if (previousSeries != null) {
+ // If the channel option of series changed, remove the existing schedules. The new
+ // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment.
if (previousSeries.getChannelOption() != series.getChannelOption()
|| (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
&& previousSeries.getChannelId() != series.getChannelId())) {
@@ -350,6 +351,18 @@ public class DvrManager {
for (ScheduledRecording schedule : schedules) {
if (schedule.isNotStarted()) {
schedulesToRemove.add(schedule);
+ } else if (schedule.isInProgress()
+ && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
+ && schedule.getChannelId() != series.getChannelId()) {
+ stopRecording(schedule);
+ }
+ }
+ List<ScheduledRecording> deletedSchedules =
+ new ArrayList<>(mDataManager.getDeletedSchedules());
+ for (ScheduledRecording deletedSchedule : deletedSchedules) {
+ if (deletedSchedule.getSeriesRecordingId() == series.getId()
+ && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) {
+ schedulesToRemove.add(deletedSchedule);
}
}
mDataManager.removeScheduledRecording(true,
@@ -363,7 +376,7 @@ public class DvrManager {
List<ScheduledRecording> schedulesToUpdate = new ArrayList<>();
for (ScheduledRecording schedule
: mDataManager.getScheduledRecordings(series.getId())) {
- if (schedule.isNotStarted()) {
+ if (schedule.isNotStarted() || schedule.isInProgress()) {
schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule)
.setPriority(priority).build());
}
@@ -373,7 +386,6 @@ public class DvrManager {
ScheduledRecording.toArray(schedulesToUpdate));
}
}
- scheduler.resumeUpdate();
}
}
@@ -400,33 +412,6 @@ public class DvrManager {
}
/**
- * Returns true, if the series recording can be removed. If a series recording is NORMAL state
- * or has recordings or schedules, it cannot be removed.
- */
- public boolean canRemoveSeriesRecording(long seriesRecordingId) {
- SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId);
- if (seriesRecording == null) {
- return false;
- }
- if (!seriesRecording.isStopped()) {
- return false;
- }
- for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
- if (r.getSeriesRecordingId() == seriesRecordingId) {
- return false;
- }
- }
- String seriesId = seriesRecording.getSeriesId();
- SoftPreconditions.checkNotNull(seriesId);
- for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
- if (seriesId.equals(r.getSeriesId())) {
- return false;
- }
- }
- return true;
- }
-
- /**
* Stops the currently recorded program
*/
public void stopRecording(final ScheduledRecording recording) {
@@ -509,13 +494,16 @@ public class DvrManager {
if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
return;
}
- new AsyncDbTask<Void, Void, Void>() {
+ new AsyncDbTask<Void, Void, Integer>() {
@Override
- protected Void doInBackground(Void... params) {
+ protected Integer doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
- int deletedCounts = resolver.delete(recordedProgram.getUri(), null, null);
+ return resolver.delete(recordedProgram.getUri(), null, null);
+ }
+
+ @Override
+ protected void onPostExecute(Integer deletedCounts) {
if (deletedCounts > 0) {
- // TODO: executeOnExecutor should be called on the main thread.
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -524,7 +512,6 @@ public class DvrManager {
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
- return null;
}
}.executeOnDbThread();
}
@@ -539,13 +526,22 @@ public class DvrManager {
dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build());
}
}
- new AsyncDbTask<Void, Void, Void>() {
+ new AsyncDbTask<Void, Void, Boolean>() {
@Override
- protected Void doInBackground(Void... params) {
+ protected Boolean doInBackground(Void... params) {
ContentResolver resolver = mAppContext.getContentResolver();
try {
resolver.applyBatch(TvContract.AUTHORITY, dbOperations);
- // TODO: executeOnExecutor should be called on the main thread.
+ } catch (RemoteException | OperationApplicationException e) {
+ Log.w(TAG, "Remove recorded programs from DB failed.", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ if (success) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -555,10 +551,7 @@ public class DvrManager {
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } catch (RemoteException | OperationApplicationException e) {
- Log.w(TAG, "Remove reocrded programs from DB failed.", e);
}
- return null;
}
}.executeOnDbThread();
}
@@ -657,6 +650,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 +676,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 +734,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 +852,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/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.
- *
- * <p> This service is responsible for:
- * <ul>
- * <li>Send record commands to TV inputs</li>
- * <li>Wake up at proper timing for recording</li>
- * <li>Deconflict schedule, handling overlapping times etc.</li>
- * <li>
- *
- * </ul>
- *
- * <p>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<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>();
// The inner map is a hash map from scheduled recording to its conflicting status, i.e.,
// the boolean value true denotes the schedule is just partially conflicting, which means
- // although there's conflictit, it might still be recorded partially.
- private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap =
- new HashMap<>();
+ // although there's conflict, it might still be recorded partially.
+ private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>();
private boolean mInitialized;
@@ -171,10 +172,9 @@ public class DvrScheduleManager {
mInputScheduleMap.remove(inputId);
}
}
- Map<ScheduledRecording, Boolean> conflictInfo =
- mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId);
if (conflictInfo != null) {
- conflictInfo.remove(schedule);
+ conflictInfo.remove(schedule.getId());
if (conflictInfo.isEmpty()) {
mInputConflictInfoMap.remove(inputId);
}
@@ -221,21 +221,11 @@ public class DvrScheduleManager {
mInputScheduleMap.remove(inputId);
}
// Update conflict list as well
- Map<ScheduledRecording, Boolean> conflictInfo =
- mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId);
if (conflictInfo != null) {
- // Compare ID because ScheduledRecording.equals() doesn't work if the state
- // is changed.
- ScheduledRecording oldSchedule = null;
- for (ScheduledRecording s : conflictInfo.keySet()) {
- if (s.getId() == schedule.getId()) {
- oldSchedule = s;
- break;
- }
- }
- if (oldSchedule != null) {
- conflictInfo.put(schedule, conflictInfo.get(oldSchedule));
- conflictInfo.remove(oldSchedule);
+ ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId());
+ if (oldConflictInfo != null) {
+ oldConflictInfo.schedule = schedule;
}
}
}
@@ -317,24 +307,25 @@ public class DvrScheduleManager {
List<ScheduledRecording> addedConflicts = new ArrayList<>();
List<ScheduledRecording> removedConflicts = new ArrayList<>();
for (String inputId : mInputScheduleMap.keySet()) {
- Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId);
+ Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId);
Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>();
- if (oldConflictsInfo != null) {
- for (ScheduledRecording r : oldConflictsInfo.keySet()) {
- oldConflictMap.put(r.getId(), r);
+ if (oldConflictInfo != null) {
+ for (ConflictInfo conflictInfo : oldConflictInfo.values()) {
+ oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule);
}
}
- Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId);
- if (conflictInfo.isEmpty()) {
+ List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId);
+ if (conflicts.isEmpty()) {
mInputConflictInfoMap.remove(inputId);
} else {
- mInputConflictInfoMap.put(inputId, conflictInfo);
- List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet());
- for (ScheduledRecording r : conflicts) {
- if (oldConflictMap.remove(r.getId()) == null) {
- addedConflicts.add(r);
+ Map<Long, ConflictInfo> conflictInfos = new HashMap<>();
+ for (ConflictInfo conflictInfo : conflicts) {
+ conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo);
+ if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) {
+ addedConflicts.add(conflictInfo.schedule);
}
}
+ mInputConflictInfoMap.put(inputId, conflictInfos);
}
removedConflicts.addAll(oldConflictMap.values());
}
@@ -565,8 +556,7 @@ public class DvrScheduleManager {
}
/**
- * Returns list of all conflicting scheduled recordings with schedules belonging to {@code
- * seriesRecording}
+ * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording}
* recording.
* <p>
* Any empty list means there is no conflicts.
@@ -581,9 +571,18 @@ public class DvrScheduleManager {
if (input == null || !input.canRecord() || input.getTunerCount() <= 0) {
return Collections.emptyList();
}
- List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings(
+ List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings(
seriesRecording.getId());
- return getConflictingSchedules(input, schedulesForSeries);
+ List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>();
+ for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) {
+ if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) {
+ availableScheduledRecordingForSeries.add(scheduledRecording);
+ }
+ }
+ if (availableScheduledRecordingForSeries.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return getConflictingSchedules(input, availableScheduledRecordingForSeries);
}
/**
@@ -617,16 +616,16 @@ public class DvrScheduleManager {
* the given input.
*/
@NonNull
- private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) {
+ private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) {
SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet");
TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId);
SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId);
if (!mInitialized || input == null) {
- return Collections.emptyMap();
+ return Collections.emptyList();
}
List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId());
if (schedules == null || schedules.isEmpty()) {
- return Collections.emptyMap();
+ return Collections.emptyList();
}
return getConflictingSchedulesInfo(schedules, input.getTunerCount());
}
@@ -645,8 +644,8 @@ public class DvrScheduleManager {
if (!mInitialized || input == null) {
return false;
}
- Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
- return conflicts != null && conflicts.containsKey(schedule);
+ Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
+ return conflicts != null && conflicts.containsKey(schedule.getId());
}
/**
@@ -664,8 +663,12 @@ public class DvrScheduleManager {
if (!mInitialized || input == null) {
return false;
}
- Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId());
- return conflicts != null && conflicts.getOrDefault(schedule, false);
+ Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId());
+ if (conflicts != null) {
+ ConflictInfo conflictInfo = conflicts.get(schedule.getId());
+ return conflictInfo != null && conflictInfo.partialConflict;
+ }
+ return false;
}
/**
@@ -813,15 +816,17 @@ public class DvrScheduleManager {
@VisibleForTesting
static List<ScheduledRecording> getConflictingSchedules(
List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
- List<ScheduledRecording> result = new ArrayList<>(
- getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet());
- Collections.sort(result, RESULT_COMPARATOR);
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (ConflictInfo conflictInfo :
+ getConflictingSchedulesInfo(schedules, tunerCount, periods)) {
+ result.add(conflictInfo.schedule);
+ }
return result;
}
@VisibleForTesting
- static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
- List<ScheduledRecording> schedules, int tunerCount) {
+ static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules,
+ int tunerCount) {
return getConflictingSchedulesInfo(schedules, tunerCount, null);
}
@@ -836,13 +841,13 @@ public class DvrScheduleManager {
* to be partially recorded under the given schedules and tuner count {@code true},
* or not {@code false}.
*/
- private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(
+ private static List<ConflictInfo> getConflictingSchedulesInfo(
List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) {
List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules);
// Sort by the same order as that in InputTaskScheduler.
Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator());
List<ScheduledRecording> recordings = new ArrayList<>();
- Map<ScheduledRecording, Boolean> conflicts = new HashMap<>();
+ Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>();
Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>();
// Simulate InputTaskScheduler.
while (!schedulesToCheck.isEmpty()) {
@@ -853,26 +858,29 @@ public class DvrScheduleManager {
if (modified2OriginalSchedules.containsKey(schedule)) {
// Schedule has been modified, which means it's already conflicted.
// Modify its state to partially conflicted.
- conflicts.put(modified2OriginalSchedules.get(schedule), true);
+ ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule);
+ conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
}
} else {
ScheduledRecording candidate = findReplaceableRecording(recordings, schedule);
if (candidate != null) {
if (!modified2OriginalSchedules.containsKey(candidate)) {
- conflicts.put(candidate, true);
+ conflicts.put(candidate, new ConflictInfo(candidate, true));
}
recordings.remove(candidate);
recordings.add(schedule);
if (modified2OriginalSchedules.containsKey(schedule)) {
// Schedule has been modified, which means it's already conflicted.
// Modify its state to partially conflicted.
- conflicts.put(modified2OriginalSchedules.get(schedule), true);
+ ScheduledRecording originalSchedule =
+ modified2OriginalSchedules.get(schedule);
+ conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true));
}
} else {
if (!modified2OriginalSchedules.containsKey(schedule)) {
// if schedule has been modified, it's already conflicted.
// No need to add it again.
- conflicts.put(schedule, false);
+ conflicts.put(schedule, new ConflictInfo(schedule, false));
}
long earliestEndTime = getEarliestEndTime(recordings);
if (earliestEndTime < schedule.getEndTimeMs()) {
@@ -912,7 +920,14 @@ public class DvrScheduleManager {
}
}
}
- return conflicts;
+ List<ConflictInfo> result = new ArrayList<>(conflicts.values());
+ Collections.sort(result, new Comparator<ConflictInfo>() {
+ @Override
+ public int compare(ConflictInfo lhs, ConflictInfo rhs) {
+ return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule);
+ }
+ });
+ return result;
}
private static void removeFinishedRecordings(List<ScheduledRecording> recordings,
@@ -954,6 +969,17 @@ public class DvrScheduleManager {
return earliest;
}
+ @VisibleForTesting
+ static class ConflictInfo {
+ public ScheduledRecording schedule;
+ public boolean partialConflict;
+
+ ConflictInfo(ScheduledRecording schedule, boolean partialConflict) {
+ this.schedule = schedule;
+ this.partialConflict = partialConflict;
+ }
+ }
+
/**
* A listener which is notified the initialization of schedule manager.
*/
@@ -970,6 +996,9 @@ public class DvrScheduleManager {
public interface OnConflictStateChangeListener {
/**
* Called when the conflicting schedules change.
+ * <p>
+ * Note that this can be called before
+ * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called.
*
* @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise
* {@code false}.
diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java
index a653b5f4..2d41d732 100644
--- a/src/com/android/tv/dvr/DvrStorageStatusManager.java
+++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java
@@ -25,6 +25,7 @@ import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.media.tv.TvContract;
+import android.media.tv.TvInputInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
@@ -36,8 +37,11 @@ import android.support.annotation.IntDef;
import android.support.annotation.WorkerThread;
import android.util.Log;
+import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
+import com.android.tv.tuner.tvinput.TunerTvInputService;
+import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.io.File;
@@ -294,7 +298,7 @@ public class DvrStorageStatusManager {
storageMounted, storageMountedDir, storageMountedCapacity);
}
- private class CleanUpDbTask extends AsyncTask<Void, Void, Void> {
+ private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> {
private final ContentResolver mContentResolver;
private CleanUpDbTask() {
@@ -302,13 +306,15 @@ public class DvrStorageStatusManager {
}
@Override
- protected Void doInBackground(Void... params) {
+ protected Boolean doInBackground(Void... params) {
@DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus();
if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
return null;
}
- List<ContentProviderOperation> ops = getDeleteOps(storageStatus
- == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL);
+ if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
+ return true;
+ }
+ List<ContentProviderOperation> ops = getDeleteOps();
if (ops == null || ops.isEmpty()) {
return null;
}
@@ -329,13 +335,28 @@ public class DvrStorageStatusManager {
}
@Override
- protected void onPostExecute(Void result) {
+ protected void onPostExecute(Boolean forgetStorage) {
+ if (forgetStorage != null && forgetStorage == true) {
+ DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager();
+ TvInputManagerHelper tvInputManagerHelper =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ List<TvInputInfo> tvInputInfoList =
+ tvInputManagerHelper.getTvInputInfos(true, false);
+ if (tvInputInfoList == null || tvInputInfoList.isEmpty()) {
+ return;
+ }
+ for (TvInputInfo info : tvInputInfoList) {
+ if (Utils.isBundledInput(info.getId())) {
+ dvrManager.forgetStorage(info.getId());
+ }
+ }
+ }
if (mCleanUpDbTask == this) {
mCleanUpDbTask = null;
}
}
- private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) {
+ private List<ContentProviderOperation> getDeleteOps() {
List<ContentProviderOperation> ops = new ArrayList<>();
try (Cursor c = mContentResolver.query(
@@ -364,7 +385,7 @@ public class DvrStorageStatusManager {
continue;
}
File recordedProgramDir = new File(dataUri.getPath());
- if (deleteAll || !recordedProgramDir.exists()) {
+ if (!recordedProgramDir.exists()) {
ops.add(ContentProviderOperation.newDelete(
TvContract.buildRecordedProgramUri(Long.parseLong(id))).build());
}
diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java
index 4eada742..da6ddb1a 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;
@@ -36,9 +37,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
* It will remember and provides previous watched position of DVR playback.
*/
public class DvrWatchedPositionManager {
- private final static String TAG = "DvrWatchedPositionManager";
- private final boolean DEBUG = false;
-
private SharedPreferences mWatchedPositions;
private final Map<Long, Set> mListeners = new HashMap<>();
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index bf72d912..129ba153 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -18,7 +18,9 @@ package com.android.tv.dvr;
import android.support.annotation.MainThread;
-import com.android.tv.dvr.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
+import com.android.tv.dvr.data.SeriesRecording;
/**
* Full data manager.
@@ -27,7 +29,7 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState;
* for internal use only. Do not call them from UI directly.
*/
@MainThread
-interface WritableDvrDataManager extends DvrDataManager {
+public interface WritableDvrDataManager extends DvrDataManager {
/**
* Adds new recordings.
*/
diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java
index 0ed6362c..2ade1dad 100644
--- a/src/com/android/tv/dvr/IdGenerator.java
+++ b/src/com/android/tv/dvr/data/IdGenerator.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import java.util.concurrent.atomic.AtomicLong;
diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java
index dd744f80..2e953a52 100644
--- a/src/com/android/tv/dvr/RecordedProgram.java
+++ b/src/com/android/tv/dvr/data/RecordedProgram.java
@@ -14,22 +14,23 @@
* limitations under the License
*/
-package com.android.tv.dvr;
-
-import static android.media.tv.TvContract.RecordedPrograms;
+package com.android.tv.dvr.data;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.media.tv.TvContentRating;
import android.media.tv.TvContract;
+import android.media.tv.TvContract.RecordedPrograms;
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.common.TvContentRatingCache;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.GenreItems;
import com.android.tv.data.InternalDataUtils;
@@ -105,7 +106,8 @@ public class RecordedProgram extends BaseProgram {
.setVideoWidth(cursor.getInt(index++))
.setVideoHeight(cursor.getInt(index++))
.setAudioLanguage(cursor.getString(index++))
- .setContentRating(cursor.getString(index++))
+ .setContentRatings(
+ TvContentRatingCache.getInstance().getRatings(cursor.getString(index++)))
.setPosterArtUri(cursor.getString(index++))
.setThumbnailUri(cursor.getString(index++))
.setSearchable(cursor.getInt(index++) == 1)
@@ -156,7 +158,8 @@ public class RecordedProgram extends BaseProgram {
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_CONTENT_RATING,
+ TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings));
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);
@@ -201,7 +204,7 @@ public class RecordedProgram extends BaseProgram {
private int mVideoWidth;
private int mVideoHeight;
private String mAudioLanguage;
- private String mContentRating;
+ private TvContentRating[] mContentRatings;
private String mPosterArtUri;
private String mThumbnailUri;
private boolean mSearchable = true;
@@ -326,8 +329,8 @@ public class RecordedProgram extends BaseProgram {
return this;
}
- public Builder setContentRating(String contentRating) {
- mContentRating = contentRating;
+ public Builder setContentRatings(TvContentRating[] contentRatings) {
+ mContentRatings = contentRatings;
return this;
}
@@ -404,15 +407,17 @@ public class RecordedProgram extends BaseProgram {
}
public RecordedProgram build() {
- // Generate the series ID for the episodic program of other TV input.
- if (TextUtils.isEmpty(mSeriesId)
- && !TextUtils.isEmpty(mEpisodeNumber)) {
+ if (TextUtils.isEmpty(mTitle)) {
+ // If title is null, series cannot be generated for this program.
+ setSeriesId(null);
+ } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) {
+ // If series ID is not set, generate it for the episodic program of other TV input.
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,
+ mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRatings,
mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes,
mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1,
mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4,
@@ -443,7 +448,7 @@ public class RecordedProgram extends BaseProgram {
.setVideoWidth(orig.getVideoWidth())
.setVideoHeight(orig.getVideoHeight())
.setAudioLanguage(orig.getAudioLanguage())
- .setContentRating(orig.getContentRating())
+ .setContentRatings(orig.getContentRatings())
.setPosterArtUri(orig.getPosterArtUri())
.setThumbnailUri(orig.getThumbnailUri())
.setSearchable(orig.isSearchable())
@@ -488,7 +493,7 @@ public class RecordedProgram extends BaseProgram {
private final int mVideoWidth;
private final int mVideoHeight;
private final String mAudioLanguage;
- private final String mContentRating;
+ private final TvContentRating[] mContentRatings;
private final String mPosterArtUri;
private final String mThumbnailUri;
private final boolean mSearchable;
@@ -507,10 +512,11 @@ public class RecordedProgram extends BaseProgram {
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) {
+ String audioLanguage, TvContentRating[] contentRatings, 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;
@@ -531,7 +537,7 @@ public class RecordedProgram extends BaseProgram {
mVideoHeight = videoHeight;
mAudioLanguage = audioLanguage;
- mContentRating = contentRating;
+ mContentRatings = contentRatings;
mPosterArtUri = posterArtUri;
mThumbnailUri = thumbnailUri;
mSearchable = searchable;
@@ -578,8 +584,10 @@ public class RecordedProgram extends BaseProgram {
return mChannelId;
}
- public String getContentRating() {
- return mContentRating;
+ @Nullable
+ @Override
+ public TvContentRating[] getContentRatings() {
+ return mContentRatings;
}
public Uri getDataUri() {
@@ -605,44 +613,12 @@ public class RecordedProgram extends BaseProgram {
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);
- }
- }
+ public String getEpisodeTitle() {
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")) {
@@ -796,7 +772,7 @@ public class RecordedProgram extends BaseProgram {
Objects.equals(mShortDescription, that.mShortDescription) &&
Objects.equals(mLongDescription, that.mLongDescription) &&
Objects.equals(mAudioLanguage, that.mAudioLanguage) &&
- Objects.equals(mContentRating, that.mContentRating) &&
+ Arrays.equals(mContentRatings, that.mContentRatings) &&
Objects.equals(mPosterArtUri, that.mPosterArtUri) &&
Objects.equals(mThumbnailUri, that.mThumbnailUri);
}
@@ -831,7 +807,8 @@ public class RecordedProgram extends BaseProgram {
", mVideoHeight=" + mVideoHeight +
", mVideoWidth=" + mVideoWidth +
", mAudioLanguage='" + mAudioLanguage + '\'' +
- ", mContentRating='" + mContentRating + '\'' +
+ ", mContentRatings='" +
+ TvContentRatingCache.contentRatingsToString(mContentRatings) + '\'' +
", mPosterArtUri=" + mPosterArtUri +
", mThumbnailUri=" + mThumbnailUri +
", mSearchable=" + mSearchable +
diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index 2bda10ea..5d11c0f3 100644
--- a/src/com/android/tv/dvr/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import android.content.ContentValues;
import android.content.Context;
@@ -22,14 +22,15 @@ 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;
@@ -43,7 +44,6 @@ import java.util.Objects;
/**
* A data class for one recording contents.
*/
-@VisibleForTesting
public final class ScheduledRecording implements Parcelable {
private static final String TAG = "ScheduledRecording";
@@ -141,7 +141,6 @@ public final class ScheduledRecording implements Parcelable {
/**
* 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()
@@ -667,23 +666,19 @@ public final class ScheduledRecording implements Parcelable {
}
/**
- * Returns the program's title withe its season and episode number.
+ * Returns the program's display title, if the program title is not null, returns program title.
+ * Otherwise returns the channel name.
*/
- public String getProgramTitleWithEpisodeNumber(Context context) {
- if (TextUtils.isEmpty(mProgramTitle)) {
+ public String getProgramDisplayTitle(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);
- }
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(mChannelId);
+ return channel != null ? channel.getDisplayName()
+ : context.getString(R.string.no_program_information);
}
-
/**
* Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}.
*/
diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java
new file mode 100644
index 00000000..89533dbb
--- /dev/null
+++ b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.data;
+
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * A plain java object which includes the season/episode number for the series recording.
+ */
+public class SeasonEpisodeNumber {
+ public final long seriesRecordingId;
+ public final String seasonNumber;
+ public final String episodeNumber;
+
+ /**
+ * Creates a new Builder with the values set from an existing {@link ScheduledRecording}.
+ */
+ public SeasonEpisodeNumber(ScheduledRecording r) {
+ this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
+ }
+
+ public SeasonEpisodeNumber(long seriesRecordingId, String seasonNumber, String episodeNumber) {
+ this.seriesRecordingId = seriesRecordingId;
+ this.seasonNumber = seasonNumber;
+ this.episodeNumber = episodeNumber;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SeasonEpisodeNumber)
+ || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) {
+ return false;
+ }
+ SeasonEpisodeNumber that = (SeasonEpisodeNumber) o;
+ return seriesRecordingId == that.seriesRecordingId
+ && Objects.equals(seasonNumber, that.seasonNumber)
+ && Objects.equals(episodeNumber, that.episodeNumber);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
+ }
+
+ @Override
+ public String toString() {
+ return "SeasonEpisodeNumber{" +
+ "seriesRecordingId=" + seriesRecordingId +
+ ", seasonNumber='" + seasonNumber +
+ ", episodeNumber=" + episodeNumber +
+ '}';
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java
index 30256dc5..a0dec4a4 100644
--- a/src/com/android/tv/dvr/SeriesInfo.java
+++ b/src/com/android/tv/dvr/data/SeriesInfo.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
/**
* Series information.
diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java
index f0690f5f..822e7320 100644
--- a/src/com/android/tv/dvr/SeriesRecording.java
+++ b/src/com/android/tv/dvr/data/SeriesRecording.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.data;
import android.content.ContentValues;
import android.database.Cursor;
@@ -26,6 +26,7 @@ import android.text.TextUtils;
import com.android.tv.data.BaseProgram;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.Utils;
@@ -128,7 +129,6 @@ public class SeriesRecording implements Parcelable {
/**
* 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)
diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
index 1a12fb23..c5383d02 100644
--- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
+++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java
@@ -21,8 +21,8 @@ import android.database.Cursor;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.NamedThreadFactory;
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 2f16ba5d..8b9481a9 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -27,8 +27,8 @@ import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrContract.Schedules;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java
index df181455..ff391959 100644
--- a/src/com/android/tv/dvr/DvrDbSync.java
+++ b/src/com/android/tv/dvr/provider/DvrDbSync.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.provider;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@@ -34,8 +34,13 @@ 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 com.android.tv.util.TvUriMatcher;
import java.util.ArrayList;
import java.util.Collections;
@@ -57,11 +62,12 @@ import java.util.Set;
*/
@MainThread
@TargetApi(Build.VERSION_CODES.N)
-class DvrDbSync {
+public class DvrDbSync {
private static final String TAG = "DvrDbSync";
private static final boolean DEBUG = false;
private final Context mContext;
+ private final DvrManager mDvrManager;
private final DvrDataManagerImpl mDataManager;
private final ChannelDataManager mChannelDataManager;
private final Queue<Long> mProgramIdQueue = new LinkedList<>();
@@ -72,12 +78,12 @@ class DvrDbSync {
@SuppressLint("SwitchIntDef")
@Override
public void onChange(boolean selfChange, Uri uri) {
- switch (TvProviderUriMatcher.match(uri)) {
- case TvProviderUriMatcher.MATCH_PROGRAM:
+ switch (TvUriMatcher.match(uri)) {
+ case TvUriMatcher.MATCH_PROGRAM:
if (DEBUG) Log.d(TAG, "onProgramsUpdated");
onProgramsUpdated();
break;
- case TvProviderUriMatcher.MATCH_PROGRAM_ID:
+ case TvUriMatcher.MATCH_PROGRAM_ID:
if (DEBUG) {
Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri));
}
@@ -129,17 +135,21 @@ class DvrDbSync {
}
};
- DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
- this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager());
+ public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
+ this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(),
+ TvApplication.getSingletons(context).getDvrManager(),
+ SeriesRecordingScheduler.getInstance(context));
}
@VisibleForTesting
DvrDbSync(Context context, DvrDataManagerImpl dataManager,
- ChannelDataManager channelDataManager) {
+ ChannelDataManager channelDataManager, DvrManager dvrManager,
+ SeriesRecordingScheduler seriesRecordingScheduler) {
mContext = context;
+ mDvrManager = dvrManager;
mDataManager = dataManager;
mChannelDataManager = channelDataManager;
- mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context);
+ mSeriesRecordingScheduler = seriesRecordingScheduler;
}
/**
@@ -273,16 +283,15 @@ class DvrDbSync {
// Check the series recording.
SeriesRecording seriesRecordingForOldSchedule =
mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
- if (program.getSeriesId() != null) {
+ if (program.isEpisodic()) {
// 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);
+ SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording(
+ program, Collections.singletonList(program),
+ SeriesRecording.STATE_SERIES_STOPPED);
builder.setSeriesRecordingId(newSeriesRecording.getId());
needUpdate = true;
} else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
@@ -306,8 +315,9 @@ class DvrDbSync {
// Old program belongs to a series but the new one doesn't.
seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
}
- // Change start time only when the recording start time has not passed.
- boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs
+ // Change start time only when the recording is not started yet.
+ boolean needToChangeStartTime =
+ schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
&& program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
if (needToChangeStartTime) {
builder.setStartTimeMs(program.getStartTimeUtcMillis());
diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
index 15ca2700..ba0aca51 100644
--- a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java
+++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.provider;
import android.annotation.TargetApi;
import android.content.Context;
@@ -24,13 +24,15 @@ import android.media.tv.TvContract.Programs;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
-import android.text.TextUtils;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
import com.android.tv.util.AsyncDbTask.CursorFilter;
import com.android.tv.util.PermissionUtils;
@@ -40,7 +42,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
-import java.util.Objects;
import java.util.Set;
/**
@@ -253,21 +254,13 @@ abstract public class EpisodicProgramLoadTask {
return sqlParams;
}
- @VisibleForTesting
- static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes,
- ScheduledEpisode episode) {
- // The episode whose season number or episode number is null will always be scheduled.
- return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber)
- && !TextUtils.isEmpty(episode.episodeNumber);
- }
-
/**
* Filter the programs which match the series recording. The episodes which the schedules are
* already created for are filtered out too.
*/
private class SeriesRecordingCursorFilter implements CursorFilter {
private final Set<Long> mDisallowedProgramIds = new HashSet<>();
- private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>();
+ private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>();
SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) {
if (!mLoadDisallowedProgram) {
@@ -282,7 +275,7 @@ abstract public class EpisodicProgramLoadTask {
if (seriesRecordingIds.contains(r.getSeriesRecordingId())
&& r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
&& r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
- mScheduledEpisodes.add(new ScheduledEpisode(r));
+ mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r));
}
}
}
@@ -306,9 +299,9 @@ abstract public class EpisodicProgramLoadTask {
}
if (programMatches) {
return mLoadScheduledEpisode
- || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode(
- seriesRecording.getId(), program.getSeasonNumber(),
- program.getEpisodeNumber()));
+ || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber(
+ seriesRecording.getId(), program.getSeasonNumber(),
+ program.getEpisodeNumber()));
}
}
return false;
@@ -333,50 +326,4 @@ abstract public class EpisodicProgramLoadTask {
public String[] selectionArgs;
public CursorFilter filter;
}
-
- /**
- * A plain java object which includes the season/episode number for the series recording.
- */
- public static class ScheduledEpisode {
- public final long seriesRecordingId;
- public final String seasonNumber;
- public final String episodeNumber;
-
- /**
- * Create a new Builder with the values set from an existing {@link ScheduledRecording}.
- */
- ScheduledEpisode(ScheduledRecording r) {
- this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
- }
-
- public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) {
- this.seriesRecordingId = seriesRecordingId;
- this.seasonNumber = seasonNumber;
- this.episodeNumber = episodeNumber;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof ScheduledEpisode)) return false;
- ScheduledEpisode that = (ScheduledEpisode) o;
- return seriesRecordingId == that.seriesRecordingId
- && Objects.equals(seasonNumber, that.seasonNumber)
- && Objects.equals(episodeNumber, that.episodeNumber);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
- }
-
- @Override
- public String toString() {
- return "ScheduledEpisode{" +
- "seriesRecordingId=" + seriesRecordingId +
- ", seasonNumber='" + seasonNumber +
- ", episodeNumber=" + episodeNumber +
- '}';
- }
- }
}
diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java
index 201e379e..8aa90116 100644
--- a/src/com/android/tv/dvr/ConflictChecker.java
+++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
import android.content.ContentUris;
@@ -37,6 +37,9 @@ import com.android.tv.common.WeakHandler;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import java.util.ArrayList;
import java.util.HashMap;
diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
new file mode 100644
index 00000000..5d324ca5
--- /dev/null
+++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java
@@ -0,0 +1,207 @@
+/*
+ * 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.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.IBinder;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+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 recording service. This service should be a foreground service and send a notification
+ * to users to do long-running recording task.
+ *
+ * <p>This service is waken up when there's a scheduled recording coming soon and at boot completed
+ * since schedules have to be loaded from databases in order to set new recording alarms, which
+ * might take a long time.
+ */
+@RequiresApi(Build.VERSION_CODES.N)
+public class DvrRecordingService extends Service {
+ private static final String TAG = "DvrRecordingService";
+ private static final boolean DEBUG = false;
+
+ private static final String DVR_NOTIFICATION_CHANNEL_ID = "dvr_notification_channel";
+ private static final int ONGOING_NOTIFICATION_ID = 1;
+ @VisibleForTesting static final String EXTRA_START_FOR_RECORDING = "start_for_recording";
+
+ private static DvrRecordingService sInstance;
+ private NotificationChannel mNotificationChannel;
+ private String mContentTitle;
+ private String mContentTextRecording;
+ private String mContentTextLoading;
+
+ /**
+ * Starts the service in foreground.
+ *
+ * @param startForRecording {@code true} if there are upcoming recordings in
+ * {@link RecordingScheduler#SOON_DURATION_IN_MS} and the service is
+ * started in foreground for those recordings.
+ */
+ @MainThread
+ static void startForegroundService(Context context, boolean startForRecording) {
+ if (sInstance == null) {
+ Intent intent = new Intent(context, DvrRecordingService.class);
+ intent.putExtra(EXTRA_START_FOR_RECORDING, startForRecording);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ } else {
+ context.startService(intent);
+ }
+ } else {
+ sInstance.startForeground(startForRecording);
+ }
+ }
+
+ @MainThread
+ static void stopForegroundIfNotRecording() {
+ if (sInstance != null) {
+ sInstance.stopForegroundIfNotRecordingInternal();
+ }
+ }
+
+ private RecurringRunner mReaperRunner;
+ private InputSessionManager mSessionManager;
+
+ @VisibleForTesting boolean mIsRecording;
+ private boolean mForeground;
+
+ @VisibleForTesting final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener =
+ new OnRecordingSessionChangeListener() {
+ @Override
+ public void onRecordingSessionChange(final boolean create, final int count) {
+ mIsRecording = count > 0;
+ if (create) {
+ startForeground(true);
+ } else {
+ stopForegroundIfNotRecordingInternal();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ TvApplication.setCurrentRunningProcess(this, true);
+ if (DEBUG) Log.d(TAG, "onCreate");
+ super.onCreate();
+ SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG);
+ sInstance = this;
+ ApplicationSingletons singletons = TvApplication.getSingletons(this);
+ WritableDvrDataManager dataManager =
+ (WritableDvrDataManager) singletons.getDvrDataManager();
+ mSessionManager = singletons.getInputSessionManager();
+ mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
+ mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1),
+ new ScheduledProgramReaper(dataManager, Clock.SYSTEM), null);
+ mReaperRunner.start();
+ mContentTitle = getString(R.string.dvr_notification_content_title);
+ mContentTextRecording = getString(R.string.dvr_notification_content_text_recording);
+ mContentTextLoading = getString(R.string.dvr_notification_content_text_loading);
+ createNotificationChannel();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")");
+ if (intent != null) {
+ startForeground(intent.getBooleanExtra(EXTRA_START_FOR_RECORDING, false));
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ mReaperRunner.stop();
+ mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener);
+ sInstance = null;
+ super.onDestroy();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @VisibleForTesting
+ protected void stopForegroundIfNotRecordingInternal() {
+ if (mForeground && !mIsRecording) {
+ stopForeground();
+ }
+ }
+
+ private void startForeground(boolean hasUpcomingRecording) {
+ if (!mForeground || hasUpcomingRecording) {
+ // We may need to update notification for upcoming recordings.
+ mForeground = true;
+ startForegroundInternal(hasUpcomingRecording);
+ }
+ }
+
+ private void stopForeground() {
+ stopForegroundInternal();
+ mForeground = false;
+ }
+
+ @VisibleForTesting
+ protected void startForegroundInternal(boolean hasUpcomingRecording) {
+ // STOPSHIP: Replace the content title with real UX strings
+ Notification.Builder builder = new Notification.Builder(this)
+ .setContentTitle(mContentTitle)
+ .setContentText(hasUpcomingRecording ? mContentTextRecording : mContentTextLoading)
+ .setSmallIcon(R.drawable.ic_dvr);
+ Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
+ builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() : builder.build();
+ startForeground(ONGOING_NOTIFICATION_ID, notification);
+ }
+
+ @VisibleForTesting
+ protected void stopForegroundInternal() {
+ stopForeground(true);
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // STOPSHIP: Replace the channel name with real UX strings
+ mNotificationChannel = new NotificationChannel(DVR_NOTIFICATION_CHANNEL_ID,
+ getString(R.string.dvr_notification_channel_name),
+ NotificationManager.IMPORTANCE_LOW);
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
+ .createNotificationChannel(mNotificationChannel);
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
index 6d2f0d43..f1c0020b 100644
--- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java
+++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java
@@ -14,21 +14,27 @@
* limitations under the License
*/
-package com.android.tv.dvr;
-
-import com.android.tv.TvApplication;
+package com.android.tv.dvr.recorder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import com.android.tv.TvApplication;
/**
* Signals the DVR to start recording shows <i>soon</i>.
*/
+@RequiresApi(Build.VERSION_CODES.N)
public class DvrStartRecordingReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
TvApplication.setCurrentRunningProcess(context, true);
- DvrRecordingService.startService(context);
+ RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler();
+ if (scheduler != null) {
+ scheduler.updateAndStartServiceIfNeeded();
+ }
}
}
diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 53c89ebc..fee4568e 100644
--- a/src/com/android/tv/dvr/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+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;
@@ -30,6 +29,10 @@ import android.util.LongSparseArray;
import com.android.tv.InputSessionManager;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Clock;
import com.android.tv.util.CompositeComparator;
@@ -121,14 +124,13 @@ public class InputTaskScheduler {
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);
+ clock, 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;
@@ -139,7 +141,7 @@ public class InputTaskScheduler {
mDataManager = (WritableDvrDataManager) dataManager;
mSessionManager = sessionManager;
mClock = clock;
- mMainThreadHandler = mainThreadHandler;
+ mMainThreadHandler = new Handler(Looper.getMainLooper());
mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory
: new RecordingTaskFactory() {
@Override
@@ -150,11 +152,7 @@ public class InputTaskScheduler {
mDataManager, mClock);
}
};
- if (workerThreadHandler == null) {
- mHandler = new WorkerThreadHandler(looper);
- } else {
- mHandler = workerThreadHandler;
- }
+ mHandler = new WorkerThreadHandler(looper);
}
/**
@@ -211,7 +209,7 @@ public class InputTaskScheduler {
&& 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.
+ // The reschedule is called in RecordingScheduler.
wrapper.mTask.cancel();
mWaitingSchedules.put(schedule.getId(), schedule);
return;
diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
index ce78e1be..cbaf46b5 100644
--- a/src/com/android/tv/dvr/Scheduler.java
+++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.app.AlarmManager;
import android.app.PendingIntent;
@@ -22,18 +22,28 @@ import android.content.Context;
import android.content.Intent;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.Build;
+import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.MainThread;
+import android.support.annotation.RequiresApi;
import android.support.annotation.VisibleForTesting;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Range;
+import com.android.tv.ApplicationSingletons;
import com.android.tv.InputSessionManager;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
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;
@@ -44,15 +54,25 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
- * The core class to manage schedule and run actual recording.
+ * The core class to manage DVR schedule and run recording task.
+ **
+ * <p> This class is responsible for:
+ * <ul>
+ * <li>Sending record commands to TV inputs</li>
+ * <li>Resolving conflicting schedules, handling overlapping recording time durations, etc.</li>
+ * </ul>
+ *
+ * <p>This should be a singleton associated with application's main process.
*/
+@RequiresApi(Build.VERSION_CODES.N)
@MainThread
-public class Scheduler extends TvInputCallback implements ScheduledRecordingListener {
- private static final String TAG = "Scheduler";
+public class RecordingScheduler extends TvInputCallback implements ScheduledRecordingListener {
+ private static final String TAG = "RecordingScheduler";
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 static final String HANDLER_THREAD_NAME = "RecordingScheduler";
+ private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1);
+ @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30);
private final Looper mLooper;
private final InputSessionManager mSessionManager;
@@ -67,7 +87,52 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>();
private long mLastStartTimePendingMs;
- public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager,
+ private OnDvrScheduleLoadFinishedListener mDvrScheduleLoadListener =
+ new OnDvrScheduleLoadFinishedListener() {
+ @Override
+ public void onDvrScheduleLoadFinished() {
+ mDataManager.removeDvrScheduleLoadFinishedListener(this);
+ if (isDbLoaded()) {
+ updateInternal();
+ }
+ }
+ };
+
+ private Listener mChannelDataLoadListener = new Listener() {
+ @Override
+ public void onLoadFinished() {
+ mChannelDataManager.removeListener(this);
+ if (isDbLoaded()) {
+ updateInternal();
+ }
+ }
+
+ @Override
+ public void onChannelListUpdated() { }
+
+ @Override
+ public void onChannelBrowsableChanged() { }
+ };
+
+ /**
+ * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks.
+ * This method should be only called once in the life-cycle of the application.
+ */
+ public static RecordingScheduler createScheduler(Context context) {
+ SoftPreconditions.checkState(
+ TvApplication.getSingletons(context).getRecordingScheduler() == null);
+ HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME);
+ handlerThread.start();
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ return new RecordingScheduler(handlerThread.getLooper(),
+ singletons.getDvrManager(), singletons.getInputSessionManager(),
+ (WritableDvrDataManager) singletons.getDvrDataManager(),
+ singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), context,
+ Clock.SYSTEM, (AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
+ }
+
+ @VisibleForTesting
+ RecordingScheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager,
WritableDvrDataManager dataManager, ChannelDataManager channelDataManager,
TvInputManagerHelper inputManager, Context context, Clock clock,
AlarmManager alarmManager) {
@@ -80,89 +145,70 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
mContext = context;
mClock = clock;
mAlarmManager = alarmManager;
- }
-
- /**
- * Starts the scheduler.
- */
- public void start() {
mDataManager.addScheduledRecordingListener(this);
mInputManager.addCallback(this);
- if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) {
+ if (isDbLoaded()) {
updateInternal();
} else {
if (!mDataManager.isDvrScheduleLoadFinished()) {
- mDataManager.addDvrScheduleLoadFinishedListener(
- new OnDvrScheduleLoadFinishedListener() {
- @Override
- public void onDvrScheduleLoadFinished() {
- mDataManager.removeDvrScheduleLoadFinishedListener(this);
- updateInternal();
- }
- });
+ mDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadListener);
}
if (!mChannelDataManager.isDbLoadFinished()) {
- mChannelDataManager.addListener(new Listener() {
- @Override
- public void onLoadFinished() {
- mChannelDataManager.removeListener(this);
- updateInternal();
- }
-
- @Override
- public void onChannelListUpdated() { }
-
- @Override
- public void onChannelBrowsableChanged() { }
- });
+ mChannelDataManager.addListener(mChannelDataLoadListener);
}
}
}
/**
- * Stops the scheduler.
+ * Start recording that will happen soon, and set the next alarm time.
*/
- public void stop() {
- for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) {
- inputTaskScheduler.stop();
+ public void updateAndStartServiceIfNeeded() {
+ if (DEBUG) Log.d(TAG, "update and start service if needed");
+ if (isDbLoaded()) {
+ updateInternal();
+ } else {
+ // updateInternal will be called when DB is loaded. Start DvrRecordingService to
+ // prevent process being killed before that.
+ DvrRecordingService.startForegroundService(mContext, false);
+ }
+ }
+
+ private void updateInternal() {
+ boolean recordingSoon = updatePendingRecordings();
+ updateNextAlarm();
+ if (recordingSoon) {
+ // Start DvrRecordingService to protect upcoming recording task from being killed.
+ DvrRecordingService.startForegroundService(mContext, true);
+ } else {
+ DvrRecordingService.stopForegroundIfNotRecording();
}
- mInputManager.removeCallback(this);
- mDataManager.removeScheduledRecordingListener(this);
}
- private void updatePendingRecordings() {
+ private boolean updatePendingRecordings() {
List<ScheduledRecording> scheduledRecordings = mDataManager
.getScheduledRecordings(new Range<>(mLastStartTimePendingMs,
- mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
+ mClock.currentTimeMillis() + SOON_DURATION_IN_MS),
ScheduledRecording.STATE_RECORDING_NOT_STARTED);
for (ScheduledRecording r : scheduledRecordings) {
scheduleRecordingSoon(r);
}
+ // update() may be called multiple times, under this situation, pending recordings may be
+ // already updated thus scheduledRecordings may have a size of 0. Therefore we also have to
+ // check mLastStartTimePendingMs to check if we have upcoming recordings and prevent the
+ // recording service being wrongly pushed back to background in updateInternal().
+ return scheduledRecordings.size() > 0
+ || (mLastStartTimePendingMs > mClock.currentTimeMillis()
+ && mLastStartTimePendingMs < mClock.currentTimeMillis() + SOON_DURATION_IN_MS);
}
- /**
- * 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() {
+ private boolean isDbLoaded() {
return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished();
}
@Override
public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules));
- if (!isInitialized()) {
+ if (!isDbLoaded()) {
return;
}
handleScheduleChange(schedules);
@@ -171,14 +217,14 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
@Override
public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules));
- if (!isInitialized()) {
+ if (!isDbLoaded()) {
return;
}
boolean needToUpdateAlarm = false;
for (ScheduledRecording schedule : schedules) {
- InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
- if (scheduler != null) {
- scheduler.removeSchedule(schedule);
+ InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
+ if (inputTaskScheduler != null) {
+ inputTaskScheduler.removeSchedule(schedule);
needToUpdateAlarm = true;
}
}
@@ -190,14 +236,14 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
@Override
public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules));
- if (!isInitialized()) {
+ if (!isDbLoaded()) {
return;
}
// Update the recordings.
for (ScheduledRecording schedule : schedules) {
- InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId());
- if (scheduler != null) {
- scheduler.updateSchedule(schedule);
+ InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId());
+ if (inputTaskScheduler != null) {
+ inputTaskScheduler.updateSchedule(schedule);
}
}
handleScheduleChange(schedules);
@@ -231,13 +277,13 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
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);
+ InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
+ if (inputTaskScheduler == null) {
+ inputTaskScheduler = new InputTaskScheduler(mContext, input, mLooper,
+ mChannelDataManager, mDvrManager, mDataManager, mSessionManager, mClock);
+ mInputSchedulerMap.put(input.getId(), inputTaskScheduler);
}
- scheduler.addSchedule(schedule);
+ inputTaskScheduler.addSchedule(schedule);
if (mLastStartTimePendingMs < schedule.getStartTimeMs()) {
mLastStartTimePendingMs = schedule.getStartTimeMs();
}
@@ -263,21 +309,21 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList
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.
+ // No need to remove input task schedule worker 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));
+ InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(inputId);
+ if (inputTaskScheduler != null) {
+ inputTaskScheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId));
}
}
@Override
public void onTvInputInfoUpdated(TvInputInfo input) {
- InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId());
- if (scheduler != null) {
- scheduler.updateTvInputInfo(input);
+ InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
+ if (inputTaskScheduler != null) {
+ inputTaskScheduler.updateTvInputInfo(input);
}
}
}
diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index c3d236b0..14888056 100644
--- a/src/com/android/tv/dvr/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.TargetApi;
import android.content.Context;
@@ -37,7 +37,10 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
-import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper;
import com.android.tv.util.Clock;
import com.android.tv.util.Utils;
@@ -51,7 +54,6 @@ import java.util.concurrent.TimeUnit;
* 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 {
@@ -256,13 +258,21 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback
public void run() {
if (TvApplication.getSingletons(mContext).getMainActivityWrapper()
.isResumed()) {
- Toast.makeText(mContext.getApplicationContext(),
- R.string.dvr_error_insufficient_space_description,
- Toast.LENGTH_LONG)
- .show();
+ ScheduledRecording scheduledRecording = mDataManager
+ .getScheduledRecording(mScheduledRecording.getId());
+ if (scheduledRecording != null) {
+ Toast.makeText(mContext.getApplicationContext(),
+ mContext.getString(R.string
+ .dvr_error_insufficient_space_description_one_recording,
+ scheduledRecording.getProgramDisplayTitle(mContext)),
+ Toast.LENGTH_LONG)
+ .show();
+ }
} else {
Utils.setRecordingFailedReason(mContext.getApplicationContext(),
TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(),
+ mScheduledRecording.getProgramDisplayTitle(mContext));
}
}
});
diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
index cd79a631..d958c4a1 100644
--- a/src/com/android/tv/dvr/ScheduledProgramReaper.java
+++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.util.Clock;
import java.util.ArrayList;
diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
index 5ed12ce8..15508c24 100644
--- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.recorder;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
@@ -23,7 +23,6 @@ import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.MainThread;
-import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
@@ -36,11 +35,19 @@ import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.data.epg.EpgFetcher;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
-import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.WritableDvrDataManager;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesInfo;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
import com.android.tv.experiments.Experiments;
+import com.android.tv.util.LocationUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -52,11 +59,11 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-import java.util.concurrent.CopyOnWriteArraySet;
import java.util.Set;
/**
- * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}.
+ * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for
+ * the {@link com.android.tv.dvr.data.SeriesRecording}.
* <p>
* The current implementation assumes that the series recordings are scheduled only for one channel.
*/
@@ -85,15 +92,13 @@ public class SeriesRecordingScheduler {
private final DvrManager mDvrManager;
private final WritableDvrDataManager mDataManager;
private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>();
- private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>();
+ private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks =
+ new LongSparseArray<>();
private final Set<String> mFetchedSeriesIds = new ArraySet<>();
private final SharedPreferences mSharedPreferences;
private boolean mStarted;
private boolean mPaused;
private final Set<Long> mPendingSeriesRecordings = new ArraySet<>();
- private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners =
- new CopyOnWriteArraySet<>();
-
private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() {
@Override
@@ -107,7 +112,7 @@ public class SeriesRecordingScheduler {
public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
// Cancel the update.
for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator();
- iter.hasNext(); ) {
+ iter.hasNext(); ) {
SeriesRecordingUpdateTask task = iter.next();
if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings,
SeriesRecording.ID_COMPARATOR).isEmpty()) {
@@ -115,6 +120,13 @@ public class SeriesRecordingScheduler {
iter.remove();
}
}
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId());
+ if (task != null) {
+ task.cancel(true);
+ mFetchSeriesInfoTasks.remove(seriesRecording.getId());
+ }
+ }
}
@Override
@@ -226,7 +238,8 @@ public class SeriesRecordingScheduler {
}
if (DEBUG) Log.d(TAG, "stop");
mStarted = false;
- for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) {
+ for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) {
+ FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i));
task.cancel(true);
}
mFetchSeriesInfoTasks.clear();
@@ -250,7 +263,7 @@ public class SeriesRecordingScheduler {
if (Experiments.CLOUD_EPG.get()) {
FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording);
task.execute();
- mFetchSeriesInfoTasks.add(task);
+ mFetchSeriesInfoTasks.put(seriesRecording.getId(), task);
}
}
@@ -363,20 +376,6 @@ public class SeriesRecordingScheduler {
}
}
- /**
- * Adds {@link OnSeriesRecordingUpdatedListener}.
- */
- public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
- mOnSeriesRecordingUpdatedListeners.add(listener);
- }
-
- /**
- * Removes {@link OnSeriesRecordingUpdatedListener}.
- */
- public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) {
- mOnSeriesRecordingUpdatedListeners.remove(listener);
- }
-
private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) {
for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) {
if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) {
@@ -403,8 +402,7 @@ public class SeriesRecordingScheduler {
/**
* @see #pickOneProgramPerEpisode(List, List)
*/
- @VisibleForTesting
- static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
+ public static LongSparseArray<List<Program>> pickOneProgramPerEpisode(
DvrDataManager dataManager, List<SeriesRecording> seriesRecordings,
List<Program> programs) {
// Initialize.
@@ -415,7 +413,7 @@ public class SeriesRecordingScheduler {
seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId());
}
// Group programs by the episode.
- Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>();
+ Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>();
for (Program program : programs) {
long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId());
if (TextUtils.isEmpty(program.getSeasonNumber())
@@ -424,17 +422,17 @@ public class SeriesRecordingScheduler {
result.get(seriesRecordingId).add(program);
continue;
}
- ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId,
+ SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId,
program.getSeasonNumber(), program.getEpisodeNumber());
- List<Program> programsForEpisode = programsForEpisodeMap.get(episode);
+ List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber);
if (programsForEpisode == null) {
programsForEpisode = new ArrayList<>();
- programsForEpisodeMap.put(episode, programsForEpisode);
+ programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode);
}
programsForEpisode.add(program);
}
// Pick one program.
- for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) {
+ for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) {
List<Program> programsForEpisode = entry.getValue();
Collections.sort(programsForEpisode, new Comparator<Program>() {
@Override
@@ -512,13 +510,6 @@ public class SeriesRecordingScheduler {
mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
}
}
- if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) {
- for (OnSeriesRecordingUpdatedListener listener
- : mOnSeriesRecordingUpdatedListeners) {
- listener.onSeriesRecordingUpdated(
- SeriesRecording.toArray(getSeriesRecordings()));
- }
- }
}
@Override
@@ -543,7 +534,7 @@ public class SeriesRecordingScheduler {
@Override
protected SeriesInfo doInBackground(Void... voids) {
- return EpgFetcher.createEpgReader(mContext)
+ return EpgFetcher.createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext))
.getSeriesInfo(mSeriesRecording.getSeriesId());
}
@@ -561,19 +552,12 @@ public class SeriesRecordingScheduler {
mFetchedSeriesIds.add(seriesInfo.getId());
updateFetchedSeries();
}
- mFetchSeriesInfoTasks.remove(this);
+ mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
}
@Override
protected void onCancelled(SeriesInfo seriesInfo) {
- mFetchSeriesInfoTasks.remove(this);
+ mFetchSeriesInfoTasks.remove(mSeriesRecording.getId());
}
}
-
- /**
- * A listener to notify when series recording are updated.
- */
- public interface OnSeriesRecordingUpdatedListener {
- void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings);
- }
}
diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java
new file mode 100644
index 00000000..ec3b5065
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/BigArguments.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.support.annotation.NonNull;
+
+import com.android.tv.common.SoftPreconditions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Stores the object to pass through activities/fragments.
+ */
+public class BigArguments {
+ private final static String TAG = "BigArguments";
+ private static Map<String, Object> sBigArgumentMap = new HashMap<>();
+
+ /**
+ * Sets the argument.
+ */
+ public static void setArgument(String name, @NonNull Object value) {
+ SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null");
+ sBigArgumentMap.put(name, value);
+ }
+
+ /**
+ * Returns the argument which is associated to the name.
+ */
+ public static Object getArgument(String name) {
+ return sBigArgumentMap.get(name);
+ }
+
+ /**
+ * Resets the arguments.
+ */
+ public static void reset() {
+ sBigArgumentMap.clear();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
new file mode 100644
index 00000000..cddece73
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.transition.ChangeImageTransform;
+import android.transition.TransitionValues;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+
+import com.android.tv.R;
+
+import java.util.Map;
+
+/**
+ * TODO: Remove this class once b/32405620 is fixed.
+ * This class is for the workaround of b/32405620 and only for the shared element transition between
+ * {@link com.android.tv.dvr.ui.browse.RecordingCardView} and
+ * {@link com.android.tv.dvr.ui.browse.DvrDetailsActivity}.
+ */
+public class ChangeImageTransformWithScaledParent extends ChangeImageTransform {
+ private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
+
+ public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ applyParentScale(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(TransitionValues transitionValues) {
+ super.captureEndValues(transitionValues);
+ applyParentScale(transitionValues);
+ }
+
+ private void applyParentScale(TransitionValues transitionValues) {
+ View view = transitionValues.view;
+ Map<String, Object> values = transitionValues.values;
+ Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX);
+ if (matrix != null && view.getId() == R.id.details_overview_image
+ && view instanceof ImageView) {
+ ImageView imageView = (ImageView) view;
+ if (imageView.getScaleType() == ScaleType.CENTER_INSIDE
+ && imageView.getDrawable() instanceof BitmapDrawable) {
+ Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
+ if (bitmap.getWidth() < imageView.getWidth()
+ && bitmap.getHeight() < imageView.getHeight()) {
+ float scale = imageView.getContext().getResources().getFraction(
+ R.fraction.lb_focus_zoom_factor_medium, 1, 1);
+ matrix.postScale(scale, scale, imageView.getWidth() / 2,
+ imageView.getHeight() / 2);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
deleted file mode 100644
index 5d8e20ff..00000000
--- a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.tv.dvr.ui;
-
-import android.content.res.Resources;
-import android.support.v17.leanback.widget.Action;
-import android.support.v17.leanback.widget.OnActionClickedListener;
-import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrManager;
-
-/**
- * {@link RecordingDetailsFragment} for current recording in DVR.
- */
-public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
- private static final int ACTION_STOP_RECORDING = 1;
-
- @Override
- protected SparseArrayObjectAdapter onCreateActionsAdapter() {
- SparseArrayObjectAdapter adapter =
- new SparseArrayObjectAdapter(new ActionPresenterSelector());
- Resources res = getResources();
- adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING,
- res.getString(R.string.epg_dvr_dialog_message_stop_recording), null,
- res.getDrawable(R.drawable.lb_ic_stop)));
- return adapter;
- }
-
- @Override
- protected OnActionClickedListener onCreateOnActionClickedListener() {
- return new OnActionClickedListener() {
- @Override
- public void onActionClicked(Action action) {
- if (action.getId() == ACTION_STOP_RECORDING) {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity())
- .getDvrManager();
- dvrManager.stopRecording(getRecording());
- }
- getActivity().finish();
- }
- };
- }
-}
diff --git a/src/com/android/tv/dvr/ui/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/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
index 9df228d1..62327870 100644
--- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java
@@ -24,15 +24,12 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
-import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.RecordedProgram;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.util.Utils;
+import com.android.tv.dvr.data.RecordedProgram;
import java.util.List;
@@ -92,7 +89,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_ANYWAY) {
getDvrManager().addSchedule(mProgram);
} else if (action.getId() == ACTION_WATCH) {
@@ -100,4 +97,23 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment {
}
dismissDialog();
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "onTrackedGuidedActionClicked";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_RECORD_ANYWAY) {
+ return "record-anyway";
+ } else if (actionId == ACTION_WATCH) {
+ return "watch-now";
+ } else if (actionId == ACTION_CANCEL) {
+ return "cancel-recording";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java
index 78f21784..6da75e55 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;
@@ -95,7 +92,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_ANYWAY) {
getDvrManager().addSchedule(mProgram);
} else if (action.getId() == ACTION_RECORD_INSTEAD) {
@@ -104,4 +101,23 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment {
}
dismissDialog();
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrAlreadyScheduledFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_RECORD_ANYWAY) {
+ return "record-anyway";
+ } else if (actionId == ACTION_RECORD_INSTEAD) {
+ return "record-instead";
+ } else if (actionId == ACTION_CANCEL) {
+ return "cancel-recording";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java
index 837d8ab2..36659412 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;
@@ -85,7 +85,7 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
long duration = mDurations.get((int) action.getId());
long startTimeMs = System.currentTimeMillis();
@@ -106,4 +106,25 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen
R.id.halfsized_dialog_host);
}
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrChannelRecordDurationOptionFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == 0) {
+ return "record-10-minutes";
+ } else if (actionId == 1) {
+ return "record-30-minutes";
+ } else if (actionId == 2) {
+ return "record-1-hour";
+ } else if (actionId == 3) {
+ return "record-3-hour";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java
index e7be4d0a..6f362e68 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;
@@ -85,7 +84,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_VIEW_SCHEDULES) {
DvrUiHelper.startSchedulesActivityForOneTimeRecordingConflict(
getContext(), getConflicts());
@@ -93,6 +92,16 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
dismissDialog();
}
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = getId();
+ if (actionId == ACTION_VIEW_SCHEDULES) {
+ return "view-schedules";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
+
String getConflictDescription() {
List<String> titles = new ArrayList<>();
HashSet<String> titleSet = new HashSet<>();
@@ -185,6 +194,11 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrProgramConflictFragment";
+ }
}
/**
@@ -236,6 +250,11 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null);
return new Guidance(title, descriptionPrefix + " " + description, null, icon);
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrChannelRecordConflictFragment";
+ }
}
/**
@@ -300,7 +319,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_CANCEL) {
ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
if (checker != null) {
@@ -319,6 +338,23 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment {
}
@Override
+ public String getTrackerPrefix() {
+ return "DvrChannelWatchConflictFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_CANCEL) {
+ return "cancel";
+ } else if (actionId == ACTION_DELETE_CONFLICT) {
+ return "delete";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
+
+ @Override
public void onDetach() {
ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker();
if (checker != null) {
diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
deleted file mode 100644
index 73ddcdd0..00000000
--- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.dvr.ui;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
-import android.support.v17.leanback.widget.GuidedAction;
-import android.text.TextUtils;
-
-import com.android.tv.R;
-import com.android.tv.TvApplication;
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.DvrDataManager;
-import com.android.tv.dvr.DvrManager;
-
-import java.util.List;
-
-public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment {
- private static final int ACTION_CANCEL = 1;
- private static final int ACTION_FORGET_STORAGE = 2;
- private String mInputId;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Bundle args = getArguments();
- if (args != null) {
- mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID);
- }
- SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId));
- super.onCreate(savedInstanceState);
- }
-
- @NonNull
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.dvr_error_forget_storage_title);
- String description = getResources().getString(
- R.string.dvr_error_forget_storage_description);
- return new Guidance(title, description, null, null);
- }
-
- @Override
- public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_CANCEL)
- .title(getResources().getString(R.string.dvr_action_error_cancel))
- .build());
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_FORGET_STORAGE)
- .title(getResources().getString(R.string.dvr_action_error_forget_storage))
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() != ACTION_FORGET_STORAGE) {
- dismissDialog();
- return;
- }
- DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
- dvrManager.forgetStorage(mInputId);
- Activity activity = getActivity();
- if (activity instanceof DvrDetailsActivity) {
- // Since we removed everything, just finish the activity.
- activity.finish();
- } else {
- dismissDialog();
- }
- }
-}
diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
index d26e6836..ab852e10 100644
--- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java
@@ -16,24 +16,43 @@
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;
import android.view.View;
import android.view.ViewGroup;
+import com.android.tv.ApplicationSingletons;
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 com.android.tv.dvr.DvrStorageStatusManager;
+
+import java.util.List;
+
+public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment {
+ /**
+ * 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;
+ public static final String UNKNOWN_DVR_ACTION = "Unknown DVR Action";
-public class DvrGuidedStepFragment extends GuidedStepFragment {
private DvrManager mDvrManager;
private OnActionClickListener mOnActionClickListener;
@@ -44,7 +63,8 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mDvrManager = TvApplication.getSingletons(context).getDvrManager();
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mDvrManager = singletons.getDvrManager();
}
@Override
@@ -64,13 +84,27 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (mOnActionClickListener != null) {
mOnActionClickListener.onActionClick(action.getId());
}
dismissDialog();
}
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_RECORD_ANYWAY) {
+ return "record-anyway";
+ } else if (actionId == ACTION_DELETE_RECORDINGS) {
+ return "delete-recordings";
+ } else if (actionId == ACTION_CANCEL_RECORDING) {
+ return "cancel-recording";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
+
protected void dismissDialog() {
if (getActivity() instanceof MainActivity) {
SafeDismissDialogFragment currentDialog =
@@ -86,4 +120,76 @@ public class DvrGuidedStepFragment extends GuidedStepFragment {
protected void setOnActionClickListener(OnActionClickListener listener) {
mOnActionClickListener = listener;
}
+
+ /**
+ * The inner guided step fragment for
+ * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
+ * .DvrNoFreeSpaceErrorDialogFragment}.
+ */
+ public static class DvrNoFreeSpaceErrorFragment extends DvrGuidedStepFragment {
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title),
+ getString(R.string.dvr_error_no_free_space_description), null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_RECORD_ANYWAY)
+ .title(R.string.dvr_action_record_anyway)
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_DELETE_RECORDINGS)
+ .title(R.string.dvr_action_delete_recordings)
+ .build());
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_CANCEL_RECORDING)
+ .title(R.string.dvr_action_record_cancel)
+ .build());
+ }
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrNoFreeSpaceErrorFragment";
+ }
+ }
+
+ /**
+ * The inner guided step fragment for
+ * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment
+ * .DvrSmallSizedStorageErrorDialogFragment}.
+ */
+ public static class DvrSmallSizedStorageErrorFragment extends DvrGuidedStepFragment {
+ @Override
+ public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getResources().getString(
+ R.string.dvr_error_small_sized_storage_title);
+ String description = getResources().getString(
+ R.string.dvr_error_small_sized_storage_description,
+ DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024
+ / 1024 / 1024);
+ return new GuidanceStylist.Guidance(title, description, null, null);
+ }
+
+ @Override
+ public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ actions.add(new GuidedAction.Builder(activity)
+ .id(GuidedAction.ACTION_ID_OK)
+ .title(android.R.string.ok)
+ .build());
+ }
+
+ @Override
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
+ dismissDialog();
+ }
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrSmallSizedStorageErrorFragment";
+ }
+ }
} \ 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..f8ef3850 100644
--- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java
@@ -29,6 +29,7 @@ import android.view.ViewGroup;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment;
import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment;
import com.android.tv.guide.ProgramGuide;
@@ -166,6 +167,17 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
}
/**
+ * A dialog fragment to show error message when there is no enough free space to record.
+ */
+ public static class DvrNoFreeSpaceErrorDialogFragment
+ extends DvrGuidedStepDialogFragment {
+ @Override
+ protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
+ return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment();
+ }
+ }
+
+ /**
* A dialog fragment to show error message when the current storage is too small to
* support DVR
*/
@@ -173,32 +185,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment {
extends DvrGuidedStepDialogFragment {
@Override
protected DvrGuidedStepFragment onCreateGuidedStepFragment() {
- return new DvrGuidedStepFragment() {
- @Override
- public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(
- R.string.dvr_error_small_sized_storage_title);
- String description = getResources().getString(
- R.string.dvr_error_small_sized_storage_description,
- DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024
- / 1024 / 1024);
- return new Guidance(title, description, null, null);
- }
-
- @Override
- public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
- Activity activity = getActivity();
- actions.add(new GuidedAction.Builder(activity)
- .id(GuidedAction.ACTION_ID_OK)
- .title(android.R.string.ok)
- .build());
- }
-
- @Override
- public void onGuidedActionClicked(GuidedAction action) {
- dismissDialog();
- }
- };
+ return new DvrGuidedStepFragment.DvrSmallSizedStorageErrorFragment();
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
index 3b1dbfa0..182416b6 100644
--- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java
@@ -17,6 +17,7 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
@@ -24,19 +25,67 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import java.util.ArrayList;
import java.util.List;
public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
- private static final int ACTION_DONE = 1;
- private static final int ACTION_OPEN_DVR = 2;
+ /**
+ * Key for the failed scheduled recordings information.
+ */
+ public static final String FAILED_SCHEDULED_RECORDING_INFOS =
+ "failed_scheduled_recording_infos";
+
+ private static final String TAG = "DvrInsufficientSpaceErrorFragment";
+
+ private static final int ACTION_VIEW_RECENT_RECORDINGS = 1;
+
+ private ArrayList<String> mFailedScheduledRecordingInfos;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ Bundle args = getArguments();
+ if (args != null) {
+ mFailedScheduledRecordingInfos =
+ args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS);
+ }
+ SoftPreconditions.checkState(
+ mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(),
+ TAG, "failed scheduled recording is null");
+ }
@Override
public Guidance onCreateGuidance(Bundle savedInstanceState) {
- String title = getResources().getString(R.string.dvr_error_insufficient_space_title);
- String description = getResources()
- .getString(R.string.dvr_error_insufficient_space_description);
+ String title;
+ String description;
+ int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size();
+ if (failedScheduledRecordingSize == 1) {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_one_recording,
+ mFailedScheduledRecordingInfos.get(0));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_one_recording,
+ mFailedScheduledRecordingInfos.get(0));
+ } else if (failedScheduledRecordingSize == 2) {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_two_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_two_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1));
+ } else {
+ title = getString(
+ R.string.dvr_error_insufficient_space_title_three_or_more_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1),
+ mFailedScheduledRecordingInfos.get(2));
+ description = getString(
+ R.string.dvr_error_insufficient_space_description_three_or_more_recordings,
+ mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1),
+ mFailedScheduledRecordingInfos.get(2));
+ }
return new Guidance(title, description, null, null);
}
@@ -44,28 +93,38 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment {
public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
Activity activity = getActivity();
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_DONE)
- .title(getResources().getString(R.string.dvr_action_error_done))
+ .clickAction(GuidedAction.ACTION_ID_OK)
.build());
- DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext())
- .getDvrDataManager();
- if (!(dvrDataManager.getRecordedPrograms().isEmpty()
- && dvrDataManager.getStartedRecordings().isEmpty()
- && dvrDataManager.getNonStartedScheduledRecordings().isEmpty()
- && dvrDataManager.getSeriesRecordings().isEmpty())) {
- actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_OPEN_DVR)
- .title(getResources().getString(R.string.dvr_action_error_open_dvr))
- .build());
+ if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) {
+ actions.add(new GuidedAction.Builder(activity)
+ .id(ACTION_VIEW_RECENT_RECORDINGS)
+ .title(getResources().getString(
+ R.string.dvr_error_insufficient_space_action_view_recent_recordings))
+ .build());
}
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_OPEN_DVR) {
- Intent intent = new Intent(getActivity(), DvrActivity.class);
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
+ if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) {
+ Intent intent = new Intent(getActivity(), DvrBrowseActivity.class);
getActivity().startActivity(intent);
}
dismissDialog();
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrInsufficientSpaceErrorFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_RECENT_RECORDINGS) {
+ return "view-recent";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
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<ViewHolder> mBoundViewHolders = new HashSet<>();
- private final OnClickListener mOnClickListener = onCreateOnClickListener();
-
- @Override
- @CallSuper
- public void onBindViewHolder(ViewHolder viewHolder, Object o) {
- viewHolder.view.setTag(o);
- viewHolder.view.setOnClickListener(mOnClickListener);
- mBoundViewHolders.add(viewHolder);
- }
-
- @Override
- @CallSuper
- public void onUnbindViewHolder(ViewHolder viewHolder) {
- mBoundViewHolders.remove(viewHolder);
- }
-
- /**
- * Unbinds all bound view holders.
- */
- public void unbindAllViewHolders() {
- // When browse fragments are destroyed, RecyclerView would not call presenters'
- // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks.
- for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) {
- onUnbindViewHolder(viewHolder);
- }
- }
-
- /**
- * Creates {@link OnClickListener} for DVR library's card views.
- */
- protected OnClickListener onCreateOnClickListener() {
- return new OnClickListener() {
- @Override
- public void onClick(View view) {
- if (view instanceof RecordingCardView) {
- RecordingCardView v = (RecordingCardView) view;
- DvrUiHelper.startDetailsActivity((Activity) v.getContext(),
- v.getTag(), v.getImageView(), false);
- }
- }
- };
- }
-} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java
index 2e2c2849..e726995f 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 final String TAG = "DvrMissingStorageError";
+
+ 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,46 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment {
public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
Activity activity = getActivity();
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_CANCEL)
- .title(getResources().getString(R.string.dvr_action_error_cancel))
+ .id(ACTION_OK)
+ .title(android.R.string.ok)
.build());
actions.add(new GuidedAction.Builder(activity)
- .id(ACTION_FORGET_STORAGE)
- .title(getResources().getString(R.string.dvr_action_error_forget_storage))
+ .id(ACTION_OPEN_STORAGE_SETTINGS)
+ .title(getResources().getString(R.string.dvr_action_error_storage_settings))
.build());
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
- if (action.getId() == ACTION_FORGET_STORAGE) {
- DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment();
- Bundle args = new Bundle();
- args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId);
- fragment.setArguments(args);
- GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host);
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
+ 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);
+ }
+ }
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrMissingStorageErrorFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_OPEN_STORAGE_SETTINGS) {
+ return "open-storage-settings";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
}
-} \ 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/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<BaseProgram> 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<BaseProgram> {
- RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) {
- super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR);
- }
-
- @Override
- long getId(BaseProgram item) {
- return item.getId();
- }
- }
-} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
index 158bd824..e4cb7243 100644
--- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java
@@ -20,7 +20,6 @@ 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;
@@ -33,15 +32,13 @@ import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.ArrayList;
import java.util.List;
-/**
- * Fragment for DVR series recording settings.
- */
-public class PrioritySettingsFragment extends GuidedStepFragment {
+/** Fragment for DVR series recording settings. */
+public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment {
/**
* Name of series recording id starting the fragment.
* Type: Long
@@ -124,7 +121,7 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == ACTION_ID_SAVE) {
DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
@@ -156,13 +153,27 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
}
@Override
+ public String getTrackerPrefix() {
+ return "DvrPrioritySettingsFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_ID_SAVE) {
+ return "save";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
+
+ @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);
@@ -248,4 +259,4 @@ public class PrioritySettingsFragment extends GuidedStepFragment {
titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL);
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java
index da6d1637..390e0928 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,
@@ -109,7 +116,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_RECORD_EPISODE) {
getDvrManager().addSchedule(mProgram);
List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules(mProgram);
@@ -139,9 +146,28 @@ 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();
}
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrSmallSizedStorageErrorFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_RECORD_EPISODE) {
+ return "record-episode";
+ } else if (actionId == ACTION_RECORD_SERIES) {
+ return "record-series";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
index f57e4b05..667af34a 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java
@@ -22,9 +22,6 @@ import android.support.v17.leanback.app.GuidedStepFragment;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.ui.SeriesDeletionFragment;
-import com.android.tv.ui.sidepanel.SettingsFragment;
/**
* Activity to show details view in DVR.
@@ -42,7 +39,7 @@ public class DvrSeriesDeletionActivity extends Activity {
setContentView(R.layout.activity_dvr_series_settings);
// Check savedInstanceState to prevent that activity is being showed with animation.
if (savedInstanceState == null) {
- SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment();
+ DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment();
deletionFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame);
}
diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
index 36e3cfc1..8bf8560f 100644
--- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java
@@ -33,9 +33,10 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrWatchedPositionManager;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.ui.GuidedActionsStylistWithDivider;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
@@ -47,7 +48,7 @@ import java.util.concurrent.TimeUnit;
/**
* Fragment for DVR series recording settings.
*/
-public class SeriesDeletionFragment extends GuidedStepFragment {
+public class DvrSeriesDeletionFragment extends GuidedStepFragment {
private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
// Since recordings' IDs are used as its check actions' IDs, which are random positive numbers,
@@ -218,8 +219,8 @@ public class SeriesDeletionFragment extends GuidedStepFragment {
private String getWatchedString(long watchedPositionMs, long durationMs) {
if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
return getResources().getString(R.string.dvr_series_watched_info_minutes,
- Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)),
- TimeUnit.MILLISECONDS.toMinutes(durationMs));
+ Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)),
+ Utils.getRoundOffMinsFromMs(durationMs));
} else {
return getResources().getString(R.string.dvr_series_watched_info_seconds,
Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)),
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
index 1173df46..2c4bb3ea 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java
@@ -25,22 +25,29 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.data.Program;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
import java.util.List;
public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
+ /**
+ * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}.
+ * Type: List<{@link Program}>
+ */
+ public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs";
+
private final static long SERIES_RECORDING_ID_NOT_SET = -1;
private final static int ACTION_VIEW_SCHEDULES = 1;
- private DvrScheduleManager mDvrScheduleManager;
private SeriesRecording mSeriesRecording;
private boolean mShowViewScheduleOption;
+ private List<Program> mPrograms;
private int mSchedulesAddedCount = 0;
private boolean mHasConflict = false;
@@ -58,22 +65,25 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
}
mShowViewScheduleOption = getArguments().getBoolean(
DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION);
- mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager();
mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager()
.getSeriesRecording(seriesRecordingId);
if (mSeriesRecording == null) {
getActivity().finish();
return;
}
+ mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS);
+ BigArguments.reset();
mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager()
.getAvailableScheduledRecording(mSeriesRecording.getId()).size();
+ DvrScheduleManager dvrScheduleManager =
+ TvApplication.getSingletons(context).getDvrScheduleManager();
List<ScheduledRecording> conflictingRecordings =
- mDvrScheduleManager.getConflictingSchedules(mSeriesRecording);
+ dvrScheduleManager.getConflictingSchedules(mSeriesRecording);
mHasConflict = !conflictingRecordings.isEmpty();
for (ScheduledRecording recording : conflictingRecordings) {
if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) {
++mInThisSeriesConflictCount;
- } else {
+ } else if (recording.getPriority() < mSeriesRecording.getPriority()) {
++mOutThisSeriesConflictCount;
}
}
@@ -106,45 +116,63 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_VIEW_SCHEDULES) {
Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class);
intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity
.TYPE_SERIES_SCHEDULE);
intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING,
mSeriesRecording);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms);
startActivity(intent);
}
getActivity().finish();
}
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrMissingStorageErrorFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_SCHEDULES) {
+ return "view-schedules";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
+
private String getDescription() {
if (!mHasConflict) {
return getResources().getQuantityString(
- R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount,
+ R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount,
mSchedulesAddedCount, mSeriesRecording.getTitle());
} else {
// mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means
// mHasConflict is false. So we don't need to check that case.
if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_this_and_other_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_this_and_other_series_conflict,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mInThisSeriesConflictCount + mOutThisSeriesConflictCount);
} else if (mInThisSeriesConflictCount != 0) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_this_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_recording_scheduled_only_this_series_conflict,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mInThisSeriesConflictCount);
} else {
if (mOutThisSeriesConflictCount == 1) {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_other_series_one_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_only_other_series_one_conflict,
mSchedulesAddedCount, mSchedulesAddedCount,
mSeriesRecording.getTitle());
} else {
- return getResources().getQuantityString(R.plurals
- .dvr_series_recording_scheduled_only_other_series_conflict,
+ return getResources().getQuantityString(
+ R.plurals.dvr_series_scheduled_only_other_series_many_conflicts,
mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(),
mOutThisSeriesConflictCount);
}
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
index 3f7671b3..6dd20b3a 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java
@@ -17,7 +17,6 @@
package com.android.tv.dvr.ui;
import android.app.Activity;
-import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
@@ -38,25 +37,34 @@ public class DvrSeriesSettingsActivity extends Activity {
/**
* Name of the boolean flag to decide if the series recording with empty schedule and recording
* will be removed.
+ * Type: boolean
*/
public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording";
/**
* Name of the boolean flag to decide if the setting fragment should be translucent.
+ * Type: boolean
*/
public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent";
/**
- * Name of the channel id list. If the channel list is given, we show the channels
- * from the values in channel option.
- * Type: Long array
+ * Name of the program list. The list contains the programs which belong to the series.
+ * Type: List<{@link com.android.tv.data.Program}>
*/
- public static final String CHANNEL_ID_LIST = "channel_id_list";
+ public static final String PROGRAM_LIST = "program_list";
/**
* Name of the boolean flag to check if the confirm dialog should show view schedule option.
+ * Type: boolean
*/
public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG =
"show_view_schedule_option_in_dialog";
+ /**
+ * Name of the current program added to series. The current program will be recorded only when
+ * the series recording is initialized from media controller. But for other case, the current
+ * program won't be recorded.
+ */
+ public static final String CURRENT_PROGRAM = "current_program";
+
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
@@ -66,7 +74,7 @@ public class DvrSeriesSettingsActivity extends Activity {
SoftPreconditions.checkArgument(seriesRecordingId != -1);
if (savedInstanceState == null) {
- SeriesSettingsFragment settingFragment = new SeriesSettingsFragment();
+ DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment();
settingFragment.setArguments(getIntent().getExtras());
GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame);
}
diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
index 6c05c9c6..f28382da 100644
--- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
@@ -17,19 +17,13 @@
package com.android.tv.dvr.ui;
import android.app.FragmentManager;
-import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.support.v17.leanback.app.GuidedStepFragment;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
import android.support.v17.leanback.widget.GuidedActionsStylist;
-import android.util.Log;
import android.util.LongSparseArray;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ProgressBar;
import com.android.tv.R;
import com.android.tv.TvApplication;
@@ -38,14 +32,14 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.SeriesRecording.ChannelOption;
-import com.android.tv.dvr.SeriesRecordingScheduler;
-import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeasonEpisodeNumber;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.data.SeriesRecording.ChannelOption;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+
import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -53,7 +47,7 @@ import java.util.Set;
/**
* Fragment for DVR series recording settings.
*/
-public class SeriesSettingsFragment extends GuidedStepFragment
+public class DvrSeriesSettingsFragment extends GuidedStepFragment
implements DvrDataManager.SeriesRecordingListener {
private static final String TAG = "SeriesSettingsFragment";
private static final boolean DEBUG = false;
@@ -66,15 +60,13 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500;
private DvrDataManager mDvrDataManager;
- private ChannelDataManager mChannelDataManager;
- private DvrManager mDvrManager;
private SeriesRecording mSeriesRecording;
private long mSeriesRecordingId;
@ChannelOption int mChannelOption;
- private Comparator<Channel> mChannelComparator;
private long mSelectedChannelId;
private int mBackStackCount;
private boolean mShowViewScheduleOptionInDialog;
+ private Program mCurrentProgram;
private String mFragmentTitle;
private String mProrityActionTitle;
@@ -84,7 +76,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private String mChannelsActionAllText;
private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>();
private List<Channel> mChannels = new ArrayList<>();
- private EpisodicProgramLoadTask mEpisodicProgramLoadTask;
+ private List<Program> mPrograms;
private GuidedAction mPriorityGuidedAction;
private GuidedAction mChannelsGuidedAction;
@@ -100,22 +92,24 @@ public class SeriesSettingsFragment extends GuidedStepFragment
getActivity().finish();
return;
}
- mDvrManager = TvApplication.getSingletons(context).getDvrManager();
mShowViewScheduleOptionInDialog = getArguments().getBoolean(
DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG);
+ mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM);
mDvrDataManager.addSeriesRecordingListener(this);
- long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST);
- mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager();
- if (channelIds == null) {
- Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
- if (channel != null) {
- mId2Channel.put(channel.getId(), channel);
- mChannels.add(channel);
- }
- collectChannelsInBackground();
- } else {
- for (long channelId : channelIds) {
- Channel channel = mChannelDataManager.getChannel(channelId);
+ mPrograms = (List<Program>) BigArguments.getArgument(
+ DvrSeriesSettingsActivity.PROGRAM_LIST);
+ BigArguments.reset();
+ if (mPrograms == null) {
+ getActivity().finish();
+ return;
+ }
+ Set<Long> channelIds = new HashSet<>();
+ ChannelDataManager channelDataManager =
+ TvApplication.getSingletons(context).getChannelDataManager();
+ for (Program program : mPrograms) {
+ long channelId = program.getChannelId();
+ if (channelIds.add(channelId)) {
+ Channel channel = channelDataManager.getChannel(channelId);
if (channel != null) {
mId2Channel.put(channel.getId(), channel);
mChannels.add(channel);
@@ -125,16 +119,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment
mChannelOption = mSeriesRecording.getChannelOption();
mSelectedChannelId = Channel.INVALID_ID;
if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) {
- Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId());
+ Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId());
if (channel != null) {
mSelectedChannelId = channel.getId();
} else {
mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL;
}
}
- mChannelComparator = new Channel.DefaultComparator(context,
- TvApplication.getSingletons(context).getTvInputManagerHelper());
- mChannels.sort(mChannelComparator);
+ mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR);
mFragmentTitle = getString(R.string.dvr_series_settings_title);
mProrityActionTitle = getString(R.string.dvr_series_settings_priority);
mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest);
@@ -144,23 +136,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
@Override
+ public void onResume() {
+ super.onResume();
+ // To avoid the order of series's priority has changed, but series doesn't get update.
+ updatePriorityGuidedAction();
+ }
+
+ @Override
public void onDetach() {
super.onDetach();
mDvrDataManager.removeSeriesRecordingListener(this);
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
- mEpisodicProgramLoadTask = null;
- }
}
@Override
public void onDestroy() {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
- if (getFragmentManager().getBackStackEntryCount() == mBackStackCount
- && getArguments()
- .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)
- && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) {
- dvrManager.removeSeriesRecording(mSeriesRecordingId);
+ if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments()
+ .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) {
+ mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId);
}
super.onDestroy();
}
@@ -178,7 +170,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment
.id(ACTION_ID_PRIORITY)
.title(mProrityActionTitle)
.build();
- updatePriorityGuidedAction(false);
actions.add(mPriorityGuidedAction);
mChannelsGuidedAction = new GuidedAction.Builder(getActivity())
@@ -204,10 +195,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onGuidedActionClicked(GuidedAction action) {
long actionId = action.getId();
if (actionId == GuidedAction.ACTION_ID_OK) {
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
- mEpisodicProgramLoadTask = null;
- }
if (mChannelOption != mSeriesRecording.getChannelOption()
|| mSeriesRecording.isStopped()
|| (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE
@@ -218,28 +205,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment
if (mSelectedChannelId != Channel.INVALID_ID) {
builder.setChannelId(mSelectedChannelId);
}
- TvApplication.getSingletons(getContext()).getDvrManager()
- .updateSeriesRecording(builder.build());
- SeriesRecordingScheduler scheduler =
- SeriesRecordingScheduler.getInstance(getContext());
- // Since dialog is used even after the fragment is closed, we should
- // use application context.
- ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString(
- R.string.dvr_series_schedules_progress_message_updating_programs));
- scheduler.addOnSeriesRecordingUpdatedListener(
- new OnSeriesRecordingUpdatedListener() {
- @Override
- public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) {
- for (SeriesRecording seriesRecording : seriesRecordings) {
- if (seriesRecording.getId() == mSeriesRecordingId) {
- dialog.dismiss();
- scheduler.removeOnSeriesRecordingUpdatedListener(this);
- showConfirmDialog();
- return;
- }
- }
- }
- });
+ DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ dvrManager.updateSeriesRecording(builder.build());
+ if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL
+ || mSelectedChannelId == mCurrentProgram.getChannelId())) {
+ dvrManager.addSchedule(mCurrentProgram);
+ }
+ updateSchedulesToSeries();
+ showConfirmDialog();
} else {
showConfirmDialog();
}
@@ -247,9 +220,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment
finishGuidedStepFragments();
} else if (actionId == ACTION_ID_PRIORITY) {
FragmentManager fragmentManager = getFragmentManager();
- PrioritySettingsFragment fragment = new PrioritySettingsFragment();
+ DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment();
Bundle args = new Bundle();
- args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID,
+ args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID,
mSeriesRecording.getId());
fragment.setArguments(args);
GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame);
@@ -281,7 +254,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
private void updateChannelsGuidedAction(boolean notifyActionChanged) {
if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) {
mChannelsGuidedAction.setDescription(mChannelsActionAllText);
- } else {
+ } else if (mId2Channel.get(mSelectedChannelId) != null){
mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId)
.getDisplayText());
}
@@ -290,7 +263,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
}
- private void updatePriorityGuidedAction(boolean notifyActionChanged) {
+ private void updatePriorityGuidedAction() {
int totalSeriesCount = 0;
int priorityOrder = 0;
for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) {
@@ -312,49 +285,38 @@ public class SeriesSettingsFragment extends GuidedStepFragment
mPriorityGuidedAction.setDescription(getString(
R.string.dvr_series_settings_priority_rank, priorityOrder + 1));
}
- if (notifyActionChanged) {
- notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
- }
+ notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY));
}
- private void collectChannelsInBackground() {
- if (mEpisodicProgramLoadTask != null) {
- mEpisodicProgramLoadTask.cancel(true);
+ private void updateSchedulesToSeries() {
+ List<Program> recordingCandidates = new ArrayList<>();
+ Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>();
+ for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) {
+ if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
+ && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
+ scheduledEpisodes.add(new SeasonEpisodeNumber(
+ r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()));
+ }
}
- mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
- @Override
- protected void onPostExecute(List<Program> programs) {
- mEpisodicProgramLoadTask = null;
- Set<Long> channelIds = new HashSet<>();
- for (Program program : programs) {
- channelIds.add(program.getChannelId());
- }
- boolean channelAdded = false;
- for (Long channelId : channelIds) {
- if (mId2Channel.get(channelId) != null) {
- continue;
- }
- Channel channel = mChannelDataManager.getChannel(channelId);
- if (channel != null) {
- channelAdded = true;
- mId2Channel.put(channelId, channel);
- mChannels.add(channel);
- if (DEBUG) Log.d(TAG, "Added channel: " + channel);
- }
- }
- if (!channelAdded) {
- return;
- }
- mChannels.sort(mChannelComparator);
- mChannelsGuidedAction.setSubActions(buildChannelSubAction());
- notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL));
- if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask");
+ for (Program program : mPrograms) {
+ // Removes current programs and scheduled episodes out, matches the channel option.
+ if (program.getStartTimeUtcMillis() >= System.currentTimeMillis()
+ && mSeriesRecording.matchProgram(program)
+ && !scheduledEpisodes.contains(new SeasonEpisodeNumber(
+ mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) {
+ recordingCandidates.add(program);
}
- }.setLoadCurrentProgram(true)
- .setLoadDisallowedProgram(true)
- .setLoadScheduledEpisode(true)
- .setIgnoreChannelOption(true);
- mEpisodicProgramLoadTask.execute();
+ }
+ if (recordingCandidates.isEmpty()) {
+ return;
+ }
+ List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode(
+ mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates)
+ .get(mSeriesRecordingId);
+ if (!programsToSchedule.isEmpty()) {
+ TvApplication.getSingletons(getContext()).getDvrManager()
+ .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule);
+ }
}
private List<GuidedAction> buildChannelSubAction() {
@@ -373,8 +335,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment
}
private void showConfirmDialog() {
- DvrUiHelper.StartSeriesScheduledDialogActivity(
- getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog);
+ DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording,
+ mShowViewScheduleOptionInDialog, mPrograms);
finishGuidedStepFragments();
}
@@ -382,16 +344,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
@Override
- public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { }
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (series.getId() == mSeriesRecording.getId()) {
+ finishGuidedStepFragments();
+ return;
+ }
+ }
+ }
@Override
public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
for (SeriesRecording seriesRecording : seriesRecordings) {
if (seriesRecording.getId() == mSeriesRecordingId) {
mSeriesRecording = seriesRecording;
- updatePriorityGuidedAction(true);
+ updatePriorityGuidedAction();
return;
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
index c3867886..baa45793 100644
--- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java
@@ -25,15 +25,12 @@ import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
-import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.data.Channel;
-import com.android.tv.data.ChannelDataManager;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -131,15 +128,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);
}
@@ -158,4 +148,19 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment {
.clickAction(GuidedAction.ACTION_ID_CANCEL)
.build());
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrStopRecordingFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_STOP) {
+ return "stop";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java
index feaa2357..7b56cfc1 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;
@@ -78,7 +78,7 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment {
}
@Override
- public void onGuidedActionClicked(GuidedAction action) {
+ public void onTrackedGuidedActionClicked(GuidedAction action) {
if (action.getId() == ACTION_STOP_SERIES_RECORDING) {
ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
DvrManager dvrManager = singletons.getDvrManager();
@@ -101,4 +101,18 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment {
}
dismissDialog();
}
+
+ @Override
+ public String getTrackerPrefix() {
+ return "DvrStopSeriesRecordingFragment";
+ }
+
+ @Override
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ if (action.getId() == ACTION_STOP_SERIES_RECORDING) {
+ return "stop";
+ } else {
+ return super.getTrackerLabelForGuidedAction(action);
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index c0d3b0c5..302fd6cd 100644
--- a/src/com/android/tv/dvr/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -14,19 +14,27 @@
* limitations under the License.
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui;
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.ProgressDialog;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.media.tv.TvInputManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
+import android.text.Html;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
import android.widget.ImageView;
import android.widget.Toast;
@@ -34,34 +42,40 @@ 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.BaseProgram;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
-import com.android.tv.dvr.ui.DvrDetailsActivity;
-import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment;
+import com.android.tv.dialog.HalfSizedDialogFragment;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrStorageStatusManager;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
+import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
-import com.android.tv.dvr.ui.DvrSchedulesActivity;
-import com.android.tv.dvr.ui.DvrSeriesDeletionActivity;
-import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity;
-import com.android.tv.dvr.ui.DvrSeriesSettingsActivity;
-import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment;
-import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
+import com.android.tv.dvr.ui.browse.DvrDetailsActivity;
+import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
+import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
/**
* A helper class for DVR UI.
@@ -69,94 +83,54 @@ import java.util.List;
@MainThread
@TargetApi(Build.VERSION_CODES.N)
public class DvrUiHelper {
- /**
- * Handles the action to create the new schedule. It returns {@code true} if the schedule is
- * added and there's no additional UI, otherwise {@code false}.
- */
- public static boolean handleCreateSchedule(MainActivity activity, Program program) {
- if (program == null) {
- return false;
- }
- DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
- if (!program.isEpisodic()) {
- // One time recording.
- dvrManager.addSchedule(program);
- if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
- DvrUiHelper.showScheduleConflictDialog(activity, program);
- return false;
- }
- } else {
- SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
- if (seriesRecording == null || seriesRecording.isStopped()) {
- DvrUiHelper.showScheduleDialog(activity, program);
- return false;
- } else {
- // Show recorded program rather than the schedule.
- RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(),
- program.getSeasonNumber(), program.getEpisodeNumber());
- if (recordedProgram != null) {
- DvrUiHelper.showAlreadyRecordedDialog(activity, program);
- return false;
- }
- ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(),
- program.getSeasonNumber(), program.getEpisodeNumber());
- if (duplicate != null
- && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
- || duplicate.getState()
- == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
- DvrUiHelper.showAlreadyScheduleDialog(activity, program);
- return false;
- }
- // Just add the schedule.
- dvrManager.addSchedule(program);
- }
- }
- return true;
+ private static final String TAG = "DvrUiHelper";
- }
+ private static ProgressDialog sProgressDialog = null;
/**
* Checks if the storage status is good for recording and shows error messages if needed.
*
- * @return true if the storage status is fine to be recorded for {@code inputId}.
+ * @param recordingRequestRunnable if the storage status is OK to record or users choose to
+ * perform the operation anyway, this Runnable will run.
*/
- public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) {
- if (!Utils.isBundledInput(inputId)) {
- return true;
- }
- DvrStorageStatusManager dvrStorageStatusManager =
- TvApplication.getSingletons(activity).getDvrStorageStatusManager();
- int status = dvrStorageStatusManager.getDvrStorageStatus();
- if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) {
- showDvrSmallSizedStorageErrorDialog(activity);
- return false;
- } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) {
- showDvrMissingStorageErrorDialog(activity, inputId);
- return false;
- } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) {
- // TODO: handle insufficient storage case.
- return true;
- } else {
- return true;
+ public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId,
+ Runnable recordingRequestRunnable) {
+ if (Utils.isBundledInput(inputId)) {
+ switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager()
+ .getDvrStorageStatus()) {
+ case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL:
+ showDvrSmallSizedStorageErrorDialog(activity);
+ return;
+ case DvrStorageStatusManager.STORAGE_STATUS_MISSING:
+ showDvrMissingStorageErrorDialog(activity);
+ return;
+ case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT:
+ showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable);
+ return;
+ }
}
+ recordingRequestRunnable.run();
}
/**
* Shows the schedule dialog.
*/
- public static void showScheduleDialog(MainActivity activity, Program program) {
+ public static void showScheduleDialog(Activity activity, Program program,
+ boolean addCurrentProgramToSeries) {
if (SoftPreconditions.checkNotNull(program) == null) {
return;
}
Bundle args = new Bundle();
args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program);
+ args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES,
+ addCurrentProgramToSeries);
showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true);
}
/**
* Shows the recording duration options dialog.
*/
- public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) {
+ public static void showChannelRecordDurationOptions(Activity activity, Channel channel) {
if (SoftPreconditions.checkNotNull(channel) == null) {
return;
}
@@ -168,7 +142,7 @@ public class DvrUiHelper {
/**
* Shows the dialog which says that the new schedule conflicts with others.
*/
- public static void showScheduleConflictDialog(MainActivity activity, Program program) {
+ public static void showScheduleConflictDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -192,20 +166,47 @@ public class DvrUiHelper {
/**
* Shows DVR insufficient space error dialog.
*/
- public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) {
- showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null);
+ public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity,
+ Set<String> failedScheduledRecordingInfoSet) {
+ Bundle args = new Bundle();
+ ArrayList<String> failedScheduledRecordingInfoArray =
+ new ArrayList<>(failedScheduledRecordingInfoSet);
+ args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS,
+ failedScheduledRecordingInfoArray);
+ showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args);
Utils.clearRecordingFailedReason(activity,
TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
+ Utils.clearFailedScheduledRecordingInfoSet(activity);
+ }
+
+ /**
+ * Shows DVR no free space error dialog.
+ *
+ * @param recordingRequestRunnable the recording request to be executed when users choose
+ * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}.
+ */
+ public static void showDvrNoFreeSpaceErrorDialog(Activity activity,
+ Runnable recordingRequestRunnable) {
+ DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment();
+ fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) {
+ recordingRequestRunnable.run();
+ } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) {
+ Intent intent = new Intent(activity, DvrBrowseActivity.class);
+ activity.startActivity(intent);
+ }
+ }
+ });
+ showDialogFragment(activity, fragment, null);
}
/**
* Shows DVR missing storage error dialog.
*/
- private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) {
- SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId));
- Bundle args = new Bundle();
- args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId);
- showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args);
+ private static void showDvrMissingStorageErrorDialog(Activity activity) {
+ showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null);
}
/**
@@ -231,7 +232,7 @@ public class DvrUiHelper {
/**
* Shows "already scheduled" dialog.
*/
- public static void showAlreadyScheduleDialog(MainActivity activity, Program program) {
+ public static void showAlreadyScheduleDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -243,7 +244,7 @@ public class DvrUiHelper {
/**
* Shows "already recorded" dialog.
*/
- public static void showAlreadyRecordedDialog(MainActivity activity, Program program) {
+ public static void showAlreadyRecordedDialog(Activity activity, Program program) {
if (program == null) {
return;
}
@@ -252,6 +253,87 @@ public class DvrUiHelper {
showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true);
}
+ /**
+ * Handle the request of recording a current program. It will handle creating schedules and
+ * shows the proper dialog and toast message respectively for timed-recording and program
+ * recording cases.
+ *
+ * @param addProgramToSeries denotes whether the program to be recorded should be added into
+ * the series recording when users choose to record the entire series.
+ */
+ public static void requestRecordingCurrentProgram(Activity activity,
+ Channel channel, Program program, boolean addProgramToSeries) {
+ if (program == null) {
+ DvrUiHelper.showChannelRecordDurationOptions(activity, channel);
+ } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
+ String msg = activity.getString(R.string.dvr_msg_current_program_scheduled,
+ program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false));
+ Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ /**
+ * Handle the request of recording a future program. It will handle creating schedules and
+ * shows the proper toast message.
+ *
+ * @param addProgramToSeries denotes whether the program to be recorded should be added into
+ * the series recording when users choose to record the entire series.
+ */
+ public static void requestRecordingFutureProgram(Activity activity,
+ Program program, boolean addProgramToSeries) {
+ if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
+ String msg = activity.getString(
+ R.string.dvr_msg_program_scheduled, program.getTitle());
+ ToastUtils.show(activity, msg, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /**
+ * Handles the action to create the new schedule. It returns {@code true} if the schedule is
+ * added and there's no additional UI, otherwise {@code false}.
+ */
+ private static boolean handleCreateSchedule(Activity activity, Program program,
+ boolean addProgramToSeries) {
+ if (program == null) {
+ return false;
+ }
+ DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager();
+ if (!program.isEpisodic()) {
+ // One time recording.
+ dvrManager.addSchedule(program);
+ if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
+ DvrUiHelper.showScheduleConflictDialog(activity, program);
+ return false;
+ }
+ } else {
+ // Show recorded program rather than the schedule.
+ RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (recordedProgram != null) {
+ DvrUiHelper.showAlreadyRecordedDialog(activity, program);
+ return false;
+ }
+ ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber());
+ if (duplicate != null
+ && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
+ || duplicate.getState()
+ == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
+ DvrUiHelper.showAlreadyScheduleDialog(activity, program);
+ return false;
+ }
+ SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
+ if (seriesRecording == null || seriesRecording.isStopped()) {
+ DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries);
+ return false;
+ } else {
+ // Just add the schedule.
+ dvrManager.addSchedule(program);
+ }
+ }
+ return true;
+ }
+
private static void showDialogFragment(Activity activity,
DvrHalfSizedDialogFragment dialogFragment, Bundle args) {
showDialogFragment(activity, dialogFragment, args, false, false);
@@ -291,6 +373,25 @@ public class DvrUiHelper {
}
/**
+ * Launches DVR playback activity for the give recorded program.
+ *
+ * @param programId the ID of the recorded program going to be played.
+ * @param seekTimeMs the seek position to initial playback.
+ * @param pinChecked {@code true} if the pin code for parental controls has already been
+ * verified, otherwise {@code false}.
+ */
+ public static void startPlaybackActivity(Context context, long programId,
+ long seekTimeMs, boolean pinChecked) {
+ Intent intent = new Intent(context, DvrPlaybackActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
+ 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);
+ context.startActivity(intent);
+ }
+
+ /**
* Shows the schedules activity to resolve the tune conflict.
*/
public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) {
@@ -341,19 +442,66 @@ public class DvrUiHelper {
/**
* Shows the series settings activity.
*
- * @param channelIds Channel ID list which has programs belonging to the series.
+ * @param programs list of programs which belong to the series.
*/
public static void startSeriesSettingsActivity(Context context, long seriesRecordingId,
- @Nullable long[] channelIds, boolean removeEmptySeriesSchedule,
- boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) {
+ @Nullable List<Program> programs, boolean removeEmptySeriesSchedule,
+ boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog,
+ Program currentProgram) {
+ SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager()
+ .getSeriesRecording(seriesRecordingId);
+ if (series == null) {
+ return;
+ }
+ if (programs != null) {
+ startSeriesSettingsActivityInternal(context, seriesRecordingId, programs,
+ removeEmptySeriesSchedule, isWindowTranslucent,
+ showViewScheduleOptionInDialog, currentProgram);
+ } else {
+ EpisodicProgramLoadTask episodicProgramLoadTask =
+ new EpisodicProgramLoadTask(context, series) {
+ @Override
+ protected void onPostExecute(List<Program> loadedPrograms) {
+ sProgressDialog.dismiss();
+ sProgressDialog = null;
+ startSeriesSettingsActivityInternal(context, seriesRecordingId,
+ loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms,
+ removeEmptySeriesSchedule, isWindowTranslucent,
+ showViewScheduleOptionInDialog, currentProgram);
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true);
+ sProgressDialog = ProgressDialog.show(context, null, context.getString(
+ R.string.dvr_series_progress_message_reading_programs), true, true,
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ episodicProgramLoadTask.cancel(true);
+ sProgressDialog = null;
+ }
+ });
+ episodicProgramLoadTask.execute();
+ }
+ }
+
+ private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId,
+ @NonNull List<Program> programs, boolean removeEmptySeriesSchedule,
+ boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog,
+ Program currentProgram) {
+ SoftPreconditions.checkState(programs != null,
+ TAG, "Start series settings activity but programs is null");
Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId);
- intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs);
intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING,
removeEmptySeriesSchedule);
intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent);
intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG,
showViewScheduleOptionInDialog);
+ intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram);
context.startActivity(intent);
}
@@ -361,7 +509,8 @@ public class DvrUiHelper {
* Shows "series recording scheduled" dialog activity.
*/
public static void StartSeriesScheduledDialogActivity(Context context,
- SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) {
+ SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog,
+ List<Program> programs) {
if (seriesRecording == null) {
return;
}
@@ -370,6 +519,9 @@ public class DvrUiHelper {
seriesRecording.getId());
intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION,
showViewScheduleOptionInDialog);
+ BigArguments.reset();
+ BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS,
+ programs);
context.startActivity(intent);
}
@@ -447,4 +599,51 @@ public class DvrUiHelper {
Utils.toTimeString(endTimeMs, false));
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
-}
+
+ /**
+ * Returns the styled schedule's title with its season and episode number.
+ */
+ public static CharSequence getStyledTitleWithEpisodeNumber(Context context,
+ ScheduledRecording schedule, int episodeNumberStyleResId) {
+ return getStyledTitleWithEpisodeNumber(context, schedule.getProgramTitle(),
+ schedule.getSeasonNumber(), schedule.getEpisodeNumber(), episodeNumberStyleResId);
+ }
+
+ /**
+ * Returns the styled program's title with its season and episode number.
+ */
+ public static CharSequence getStyledTitleWithEpisodeNumber(Context context,
+ BaseProgram program, int episodeNumberStyleResId) {
+ return getStyledTitleWithEpisodeNumber(context, program.getTitle(),
+ program.getSeasonNumber(), program.getEpisodeNumber(), episodeNumberStyleResId);
+ }
+
+ @NonNull
+ public static CharSequence getStyledTitleWithEpisodeNumber(Context context, String title,
+ String seasonNumber, String episodeNumber, int episodeNumberStyleResId) {
+ if (TextUtils.isEmpty(title)) {
+ return "";
+ }
+ SpannableStringBuilder builder;
+ if (TextUtils.isEmpty(seasonNumber) || seasonNumber.equals("0")) {
+ builder = TextUtils.isEmpty(episodeNumber) ? new SpannableStringBuilder(title) :
+ new SpannableStringBuilder(Html.fromHtml(
+ context.getString(R.string.program_title_with_episode_number_no_season,
+ title, episodeNumber)));
+ } else {
+ builder = new SpannableStringBuilder(Html.fromHtml(
+ context.getString(R.string.program_title_with_episode_number,
+ title, seasonNumber, episodeNumber)));
+ }
+ Object[] spans = builder.getSpans(0, builder.length(), Object.class);
+ if (spans.length > 0) {
+ if (episodeNumberStyleResId != 0) {
+ builder.setSpan(new TextAppearanceSpan(context, episodeNumberStyleResId),
+ builder.getSpanStart(spans[0]), builder.getSpanEnd(spans[0]),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ builder.removeSpan(spans[0]);
+ }
+ return new SpannableString(builder);
+ }
+} \ 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/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/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/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/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
index 393a5ff3..8c0af9ed 100644
--- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
+++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java
@@ -20,11 +20,15 @@ import android.support.annotation.VisibleForTesting;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.PresenterSelector;
+import com.android.tv.common.SoftPreconditions;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Keeps a set of items sorted
@@ -35,16 +39,18 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
private final Comparator<T> mComparator;
private final int mMaxItemCount;
private int mExtraItemCount;
+ private final Set<Long> mIds = new HashSet<>();
- SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
+ public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) {
this(presenterSelector, comparator, Integer.MAX_VALUE);
}
- SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator,
+ public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator,
int maxItemCount) {
super(presenterSelector);
mComparator = comparator;
mMaxItemCount = maxItemCount;
+ setHasStableIds(true);
}
/**
@@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
final void setInitialItems(List<T> items) {
List<T> itemsCopy = new ArrayList<>(items);
Collections.sort(itemsCopy, mComparator);
- addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size())));
+ for (T item : itemsCopy) {
+ add(item, true);
+ if (size() == mMaxItemCount) {
+ break;
+ }
+ }
}
/**
@@ -82,6 +93,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
* the end to save search time.
*/
public final void add(T item, boolean insertToEnd) {
+ long newItemId = getId(item);
+ SoftPreconditions.checkState(!mIds.contains(newItemId));
+ mIds.add(newItemId);
int i;
if (insertToEnd) {
i = findInsertPosition(item);
@@ -89,8 +103,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
i = findInsertPositionBinary(item);
}
super.add(i, item);
- if (size() > mMaxItemCount + mExtraItemCount) {
- removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount);
+ if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) {
+ Object removedItem = get(mMaxItemCount);
+ remove(removedItem);
}
}
@@ -100,48 +115,97 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter {
* They will be presented in their insertion order.
*/
public int addExtraItem(T item) {
+ long newItemId = getId(item);
+ SoftPreconditions.checkState(!mIds.contains(newItemId));
+ mIds.add(newItemId);
super.add(item);
return ++mExtraItemCount;
}
+ @Override
+ public boolean remove(Object item) {
+ return removeWithId((T) item);
+ }
+
/**
* Removes an item which has the same ID as {@code item}.
*/
public boolean removeWithId(T item) {
- int index = indexWithTypeAndId(item);
- return index >= 0 && index < size() && remove(get(index));
+ int index = indexWithId(item);
+ return index >= 0 && index < size() && removeItems(index, 1) == 1;
+ }
+
+ @Override
+ public int removeItems(int position, int count) {
+ int upperBound = Math.min(position + count, size());
+ for (int i = position; i < upperBound; i++) {
+ mIds.remove(getId((T) get(i)));
+ }
+ if (upperBound > size() - mExtraItemCount) {
+ mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position);
+ }
+ return super.removeItems(position, count);
+ }
+
+ @Override
+ public void replace(int position, Object item) {
+ boolean wasExtra = position >= size() - mExtraItemCount;
+ removeItems(position, 1);
+ if (!wasExtra) {
+ add(item);
+ } else {
+ addExtraItem((T) item);
+ }
+ }
+
+ @Override
+ public void clear() {
+ mIds.clear();
+ super.clear();
}
/**
- * Change an item in the list.
+ * Changes an item in the list.
* @param item The item to change.
*/
public final void change(T item) {
- int oldIndex = indexWithTypeAndId(item);
+ int oldIndex = indexWithId(item);
if (oldIndex != -1) {
T old = (T) get(oldIndex);
if (mComparator.compare(old, item) == 0) {
replace(oldIndex, item);
return;
}
- removeItems(oldIndex, 1);
+ remove(old);
}
add(item);
}
/**
+ * Checks whether the item is in the list.
+ */
+ public final boolean contains(T item) {
+ return indexWithId(item) != -1;
+ }
+
+ @Override
+ public long getId(int position) {
+ return getId((T) get(position));
+ }
+
+ /**
* Returns the id of the the given {@code item}, which will be used in {@link #change} to
* decide if the given item is already existed in the adapter.
*
* The id must be stable.
*/
- abstract long getId(T item);
+ protected abstract long getId(T item);
- private int indexWithTypeAndId(T item) {
+ private int indexWithId(T item) {
long id = getId(item);
for (int i = 0; i < size() - mExtraItemCount; i++) {
T r = (T) get(i);
- if (r.getClass() == item.getClass() && getId(r) == id) {
+ if (getId(r) == id) {
return i;
}
}
diff --git a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
new file mode 100644
index 00000000..5fe9c478
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.support.v17.leanback.app.GuidedStepFragment;
+import android.support.v17.leanback.widget.GuidedAction;
+
+import com.android.tv.TvApplication;
+import com.android.tv.analytics.Tracker;
+
+/** A {@link GuidedStepFragment} with {@link Tracker} for analytics. */
+public abstract class TrackedGuidedStepFragment extends GuidedStepFragment {
+ private Tracker mTracker;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mTracker = TvApplication.getSingletons(context).getAnalytics().getDefaultTracker();
+ }
+
+ @Override
+ public void onDetach() {
+ mTracker = null;
+ super.onDetach();
+ }
+
+ @Override
+ public final void onGuidedActionClicked(GuidedAction action) {
+ super.onGuidedActionClicked(action);
+ if (mTracker != null) {
+ mTracker.sendMenuClicked(
+ getTrackerPrefix() + "-action-" + getTrackerLabelForGuidedAction(action));
+ }
+ onTrackedGuidedActionClicked(action);
+ }
+
+ public String getTrackerLabelForGuidedAction(GuidedAction action) {
+ long actionId = action.getId();
+ if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ return "cancel";
+ } else if (actionId == GuidedAction.ACTION_ID_NEXT) {
+ return "next";
+ } else if (actionId == GuidedAction.ACTION_ID_CURRENT) {
+ return "current";
+ } else if (actionId == GuidedAction.ACTION_ID_OK) {
+ return "ok";
+ } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
+ return "cancel";
+ } else if (actionId == GuidedAction.ACTION_ID_FINISH) {
+ return "finish";
+ } else if (actionId == GuidedAction.ACTION_ID_CONTINUE) {
+ return "continue";
+ } else if (actionId == GuidedAction.ACTION_ID_YES) {
+ return "yes";
+ } else if (actionId == GuidedAction.ACTION_ID_NO) {
+ return "no";
+ } else {
+ return "unknown-" + actionId;
+ }
+ }
+
+ /** Delegated from {@link #onGuidedActionClicked(GuidedAction)} */
+ public abstract void onTrackedGuidedActionClicked(GuidedAction action);
+
+ /** The prefix used for analytics tracking, Usually the class name. */
+ public abstract String getTrackerPrefix();
+}
diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
index 8b8cd5c5..38a78f5d 100644
--- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.R;
@@ -110,11 +110,7 @@ class ActionPresenterSelector extends PresenterSelector {
.getDimensionPixelSize(R.dimen.lb_action_padding_horizontal);
vh.view.setPaddingRelative(padding, 0, padding, 0);
}
- if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) {
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null);
- } else {
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
- }
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
CharSequence line1 = action.getLabel1();
CharSequence line2 = action.getLabel2();
@@ -130,7 +126,7 @@ class ActionPresenterSelector extends PresenterSelector {
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
ActionViewHolder vh = (ActionViewHolder) viewHolder;
- vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null);
vh.view.setPadding(0, 0, 0, 0);
vh.mAction = null;
}
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
new file mode 100644
index 00000000..bf18ddc0
--- /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.dvr_detail_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..c1fa05d7
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -0,0 +1,317 @@
+/*
+ * 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.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.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+
+/**
+ * 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 boolean mUsingChannelLogo;
+
+ static DetailsContent createFromRecordedProgram(Context context,
+ RecordedProgram recordedProgram) {
+ return new DetailsContent.Builder()
+ .setChannelId(recordedProgram.getChannelId())
+ .setProgramTitle(recordedProgram.getTitle())
+ .setSeasonNumber(recordedProgram.getSeasonNumber())
+ .setEpisodeNumber(recordedProgram.getEpisodeNumber())
+ .setStartTimeUtcMillis(recordedProgram.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(recordedProgram.getEndTimeUtcMillis())
+ .setDescription(TextUtils.isEmpty(recordedProgram.getLongDescription())
+ ? recordedProgram.getDescription() : recordedProgram.getLongDescription())
+ .setPosterArtUri(recordedProgram.getPosterArtUri())
+ .setThumbnailUri(recordedProgram.getThumbnailUri())
+ .build(context);
+ }
+
+ static DetailsContent createFromSeriesRecording(Context context,
+ SeriesRecording seriesRecording) {
+ return new DetailsContent.Builder()
+ .setChannelId(seriesRecording.getChannelId())
+ .setTitle(seriesRecording.getTitle())
+ .setDescription(TextUtils.isEmpty(seriesRecording.getLongDescription())
+ ? seriesRecording.getDescription() : seriesRecording.getLongDescription())
+ .setPosterArtUri(seriesRecording.getPosterUri())
+ .setThumbnailUri(seriesRecording.getPhotoUri())
+ .build(context);
+ }
+
+ static DetailsContent createFromScheduledRecording(Context context,
+ ScheduledRecording scheduledRecording) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(scheduledRecording.getChannelId());
+ String description = !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) ?
+ scheduledRecording.getProgramDescription()
+ : scheduledRecording.getProgramLongDescription();
+ if (TextUtils.isEmpty(description)) {
+ description = channel != null ? channel.getDescription() : null;
+ }
+ return new DetailsContent.Builder()
+ .setChannelId(scheduledRecording.getChannelId())
+ .setProgramTitle(scheduledRecording.getProgramTitle())
+ .setSeasonNumber(scheduledRecording.getSeasonNumber())
+ .setEpisodeNumber(scheduledRecording.getEpisodeNumber())
+ .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs())
+ .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs())
+ .setDescription(description)
+ .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
+ .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
+ .build(context);
+ }
+
+ 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;
+ }
+
+ /**
+ * Returns if image URIs are from its channels' logo.
+ */
+ public boolean isUsingChannelLogo() {
+ return mUsingChannelLogo;
+ }
+
+ /**
+ * 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;
+ mUsingChannelLogo = other.mUsingChannelLogo;
+ }
+
+ /**
+ * A class for building details content.
+ */
+ public static final class Builder {
+ private final DetailsContent mDetailsContent;
+
+ private long mChannelId;
+ private String mProgramTitle;
+ private String mSeasonNumber;
+ private String mEpisodeNumber;
+ private String mPosterArtUri;
+ private String mThumbnailUri;
+
+ 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;
+ }
+
+ private Builder setProgramTitle(String programTitle) {
+ mProgramTitle = programTitle;
+ return this;
+ }
+
+ private Builder setSeasonNumber(String seasonNumber) {
+ mSeasonNumber = seasonNumber;
+ return this;
+ }
+
+ private Builder setEpisodeNumber(String episodeNumber) {
+ mEpisodeNumber = episodeNumber;
+ return this;
+ }
+
+ private Builder setChannelId(long channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ private Builder setPosterArtUri(String posterArtUri) {
+ mPosterArtUri = posterArtUri;
+ return this;
+ }
+
+ private Builder setThumbnailUri(String thumbnailUri) {
+ mThumbnailUri = thumbnailUri;
+ return this;
+ }
+
+ private void createStyledTitle(Context context, Channel channel) {
+ CharSequence title = DvrUiHelper.getStyledTitleWithEpisodeNumber(context,
+ mProgramTitle, mSeasonNumber, mEpisodeNumber,
+ R.style.text_appearance_card_view_episode_number);
+ if (TextUtils.isEmpty(title)) {
+ mDetailsContent.mTitle = channel != null ? channel.getDisplayName()
+ : context.getResources().getString(R.string.no_program_information);
+ } else {
+ mDetailsContent.mTitle = title;
+ }
+ }
+
+ private void createImageUris(@Nullable Channel channel) {
+ mDetailsContent.mLogoImageUri = null;
+ mDetailsContent.mBackgroundImageUri = null;
+ mDetailsContent.mUsingChannelLogo = false;
+ if (!TextUtils.isEmpty(mPosterArtUri) && !TextUtils.isEmpty(mThumbnailUri)) {
+ mDetailsContent.mLogoImageUri = mPosterArtUri;
+ mDetailsContent.mBackgroundImageUri = mThumbnailUri;
+ } else if (!TextUtils.isEmpty(mPosterArtUri)) {
+ // thumbnailUri is empty
+ mDetailsContent.mLogoImageUri = mPosterArtUri;
+ mDetailsContent.mBackgroundImageUri = mPosterArtUri;
+ } else if (!TextUtils.isEmpty(mThumbnailUri)) {
+ // posterArtUri is empty
+ mDetailsContent.mLogoImageUri = mThumbnailUri;
+ mDetailsContent.mBackgroundImageUri = mThumbnailUri;
+ }
+ if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) {
+ String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId())
+ .toString();
+ mDetailsContent.mLogoImageUri = channelLogoUri;
+ mDetailsContent.mBackgroundImageUri = channelLogoUri;
+ mDetailsContent.mUsingChannelLogo = true;
+ }
+ }
+
+ /**
+ * Builds details content.
+ */
+ public DetailsContent build(Context context) {
+ Channel channel = TvApplication.getSingletons(context).getChannelDataManager()
+ .getChannel(mChannelId);
+ if (mDetailsContent.mTitle == null) {
+ createStyledTitle(context, channel);
+ }
+ if (mDetailsContent.mBackgroundImageUri == null
+ && mDetailsContent.mLogoImageUri == null) {
+ createImageUris(channel);
+ }
+ 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/browse/DetailsContentPresenter.java
index 175f05bc..09b57887 100644
--- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -14,13 +14,14 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
+import android.content.Context;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.support.v17.leanback.widget.Presenter;
@@ -29,6 +30,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -38,13 +40,14 @@ import com.android.tv.util.Utils;
/**
* An {@link Presenter} for rendering a detailed description of an DVR item.
- * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}.
+ * Typically this Presenter will be used in a
+ * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}.
* Most codes of this class is originated from
* {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
* The latter class are re-used to provide a customized version of
* {@link android.support.v17.leanback.widget.DetailsOverviewRow}.
*/
-public class DetailsContentPresenter extends Presenter {
+class DetailsContentPresenter extends Presenter {
/**
* The ViewHolder for the {@link DetailsContentPresenter}.
*/
@@ -82,25 +85,38 @@ public class DetailsContentPresenter extends Presenter {
return false;
}
final int bodyLines = mBody.getLineCount();
- final int maxLines = mFullTextMode ? bodyLines :
+ int maxLines = mFullTextMode ? bodyLines :
(mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines);
if (bodyLines > maxLines) {
mReadMoreView.setVisibility(View.VISIBLE);
mDescriptionContainer.setFocusable(true);
+ mDescriptionContainer.setClickable(true);
mDescriptionContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mFullTextMode = true;
mReadMoreView.setVisibility(View.GONE);
- mDescriptionContainer.setFocusable(false);
+ mDescriptionContainer.setFocusable((
+ (AccessibilityManager) view.getContext()
+ .getSystemService(
+ Context.ACCESSIBILITY_SERVICE))
+ .isEnabled());
+ mDescriptionContainer.setClickable(false);
mDescriptionContainer.setOnClickListener(null);
+ int oldMaxLines = mBody.getMaxLines();
mBody.setMaxLines(bodyLines);
// Minus 1 from line difference to eliminate the space
// originally occupied by "READ MORE"
- showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing);
+ showFullText((bodyLines - oldMaxLines - 1) * mBodyLineSpacing);
}
});
}
+ if (mReadMoreView.getVisibility() == View.VISIBLE
+ && mSubtitle.getVisibility() == View.VISIBLE) {
+ // If both "READ MORE" and subtitle is shown, the capable maximum lines
+ // will be one line less.
+ maxLines -= 1;
+ }
if (mBody.getMaxLines() != maxLines) {
mBody.setMaxLines(maxLines);
return false;
@@ -113,11 +129,30 @@ public class DetailsContentPresenter extends Presenter {
public ViewHolder(final View view) {
super(view);
+ view.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // In case predraw listener was removed in detach, make sure
+ // we have the proper layout.
+ addPreDrawListener();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removePreDrawListener();
+ }
+ });
mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
mDescriptionContainer =
(LinearLayout) view.findViewById(R.id.dvr_details_description_container);
+ // We have to explicitly set focusable to true here for accessibility, since we might
+ // set the view's focusable state when we need to show "READ MORE", which would remove
+ // the default focusable state for accessibility.
+ mDescriptionContainer.setFocusable(((AccessibilityManager) view.getContext()
+ .getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled());
mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more);
FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle);
@@ -129,7 +164,7 @@ public class DetailsContentPresenter extends Presenter {
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);
+ R.dimen.dvr_details_description_under_subtitle_baseline_margin);
mTitleLineSpacing = view.getResources().getDimensionPixelSize(
R.dimen.lb_details_description_title_line_spacing);
@@ -276,22 +311,6 @@ public class DetailsContentPresenter extends Presenter {
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { }
- @Override
- public void onViewAttachedToWindow(Presenter.ViewHolder holder) {
- // In case predraw listener was removed in detach, make sure
- // we have the proper layout.
- ViewHolder vh = (ViewHolder) holder;
- vh.addPreDrawListener();
- super.onViewAttachedToWindow(holder);
- }
-
- @Override
- public void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
- ViewHolder vh = (ViewHolder) holder;
- vh.removePreDrawListener();
- super.onViewDetachedFromWindow(holder);
- }
-
private void setTopMargin(View view, int topMargin) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
lp.topMargin = topMargin;
diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
index 6714ecd3..82fe9ce3 100644
--- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.graphics.drawable.BitmapDrawable;
@@ -26,7 +26,7 @@ import android.support.v17.leanback.app.BackgroundManager;
/**
* The Background Helper.
*/
-public class DetailsViewBackgroundHelper {
+class DetailsViewBackgroundHelper {
// Background delay serves to avoid kicking off expensive bitmap loading
// in case multiple backgrounds are set in quick succession.
private static final int SET_BACKGROUND_DELAY_MS = 100;
diff --git a/src/com/android/tv/dvr/ui/DvrActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
index 45fb1cf1..07eec107 100644
--- a/src/com/android/tv/dvr/ui/DvrActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -14,9 +14,11 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
+import android.content.Intent;
+import android.media.tv.TvInputManager;
import android.os.Bundle;
import com.android.tv.R;
@@ -25,11 +27,26 @@ import com.android.tv.TvApplication;
/**
* {@link android.app.Activity} for DVR UI.
*/
-public class DvrActivity extends Activity {
+public class DvrBrowseActivity extends Activity {
+ private DvrBrowseFragment mFragment;
+
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
super.onCreate(savedInstanceState);
setContentView(R.layout.dvr_main);
+ mFragment = (DvrBrowseFragment) getFragmentManager().findFragmentById(R.id.dvr_frame);
+ handleIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ handleIntent(intent);
+ }
+
+ private void handleIntent(Intent intent) {
+ if (TvInputManager.ACTION_VIEW_RECORDING_SCHEDULES.equals(intent.getAction())) {
+ mFragment.showScheduledRow();
+ }
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index a6dd31d1..cb3a5745 100644
--- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -14,10 +14,9 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
-import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Bundle;
import android.os.Handler;
import android.support.v17.leanback.app.BrowseFragment;
@@ -25,11 +24,11 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.TitleViewAdapter;
-import android.text.TextUtils;
import android.util.Log;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
@@ -42,9 +41,10 @@ import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.RecordedProgram;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -64,12 +64,16 @@ public class DvrBrowseFragment extends BrowseFragment implements
private static final int MAX_RECENT_ITEM_COUNT = 10;
private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
+ private boolean mShouldShowScheduleRow;
+ private boolean mEntranceTransitionEnded;
+
private RecordedProgramAdapter mRecentAdapter;
private ScheduleAdapter mScheduleAdapter;
private SeriesAdapter mSeriesAdapter;
private RecordedProgramAdapter[] mGenreAdapters =
new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
private ListRow mRecentRow;
+ private ListRow mScheduledRow;
private ListRow mSeriesRow;
private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
private List<String> mGenreLabels;
@@ -79,6 +83,20 @@ public class DvrBrowseFragment extends BrowseFragment implements
private ClassPresenterSelector mPresenterSelector;
private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
private final Handler mHandler = new Handler();
+ private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+ new OnGlobalFocusChangeListener() {
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (oldFocus instanceof RecordingCardView) {
+ ((RecordingCardView) oldFocus).expandTitle(false, true);
+ }
+ if (newFocus instanceof RecordingCardView) {
+ // If the header transition is ongoing, expand cards immediately without
+ // animation to make a smooth transition.
+ ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition());
+ }
+ }
+ };
private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() {
@Override
@@ -104,7 +122,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
};
- private final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() {
+ private static final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() {
@Override
public int compare(Object lhs, Object rhs) {
if (lhs instanceof ScheduledRecording) {
@@ -128,7 +146,7 @@ public class DvrBrowseFragment extends BrowseFragment implements
public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) {
if (mScheduleAdapter != null) {
for (ScheduledRecording schedule : schedules) {
- onScheduledRecordingStatusChanged(schedule);
+ onScheduledRecordingConflictStatusChanged(schedule);
}
}
}
@@ -154,16 +172,12 @@ public class DvrBrowseFragment extends BrowseFragment implements
new ScheduledRecordingPresenter(context))
.addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context))
.addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context))
- .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter());
+ .addClassPresenter(FullScheduleCardHolder.class,
+ new FullSchedulesCardPresenter(context));
mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
mGenreLabels.add(getString(R.string.dvr_main_others));
- setupUiElements();
- setupAdapters();
- mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
- prepareEntranceTransition();
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- } else {
+ prepareUiElements();
+ if (!startBrowseIfDvrInitialized()) {
if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
}
@@ -174,6 +188,19 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
@Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ }
+
+ @Override
+ public void onDestroyView() {
+ getView().getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ super.onDestroyView();
+ }
+
+ @Override
public void onDestroy() {
if (DEBUG) Log.d(TAG, "onDestroy");
mHandler.removeCallbacks(mUpdateRowsRunnable);
@@ -195,25 +222,13 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public void onDvrScheduleLoadFinished() {
- List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings();
- onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings));
- List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings();
- onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings));
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- }
+ startBrowseIfDvrInitialized();
mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
}
@Override
public void onRecordedProgramLoadFinished() {
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, true);
- }
- updateRows();
- if (mDvrDataManager.isInitialized()) {
- startEntranceTransition();
- }
+ startBrowseIfDvrInitialized();
mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
}
@@ -270,6 +285,18 @@ public class DvrBrowseFragment extends BrowseFragment implements
}
}
+ private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (needToShowScheduledRecording(schedule)) {
+ if (mScheduleAdapter.contains(schedule)) {
+ mScheduleAdapter.change(schedule);
+ }
+ } else {
+ mScheduleAdapter.removeWithId(schedule);
+ }
+ }
+ }
+
@Override
public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
@@ -295,44 +322,80 @@ public class DvrBrowseFragment extends BrowseFragment implements
super.showTitle(flags);
}
- private void setupUiElements() {
+ @Override
+ protected void onEntranceTransitionEnd() {
+ super.onEntranceTransitionEnd();
+ if (mShouldShowScheduleRow) {
+ showScheduledRowInternal();
+ }
+ mEntranceTransitionEnded = true;
+ }
+
+ void showScheduledRow() {
+ if (!mEntranceTransitionEnded) {
+ setHeadersState(HEADERS_HIDDEN);
+ mShouldShowScheduleRow = true;
+ } else {
+ showScheduledRowInternal();
+ }
+ }
+
+ private void showScheduledRowInternal() {
+ setSelectedPosition(mRowsAdapter.indexOf(mScheduledRow), true, null);
+ if (getHeadersState() == HEADERS_ENABLED) {
+ startHeadersTransition(false);
+ }
+ mShouldShowScheduleRow = false;
+ }
+
+ private void prepareUiElements() {
setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
setHeadersState(HEADERS_ENABLED);
setHeadersTransitionOnBackEnabled(false);
setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
+ mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext()));
+ setAdapter(mRowsAdapter);
+ prepareEntranceTransition();
}
- private void setupAdapters() {
- mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
- mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
- mSeriesAdapter = new SeriesAdapter();
- for (int i = 0; i < mGenreAdapters.length; i++) {
- mGenreAdapters[i] = new RecordedProgramAdapter();
- }
- // Schedule Recordings.
- List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
- onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
- mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
- // Recorded Programs.
- for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
- handleRecordedProgramAdded(recordedProgram, false);
- }
- // Series Recordings. Series recordings should be added after recorded programs, because
- // we build series recordings' latest program information while adding recorded programs.
- List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
- handleSeriesRecordingsAdded(recordings);
- mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
- mRecentRow = new ListRow(new HeaderItem(
- getString(R.string.dvr_main_recent)), mRecentAdapter);
- mRowsAdapter.add(new ListRow(new HeaderItem(
- getString(R.string.dvr_main_scheduled)), mScheduleAdapter));
- mSeriesRow = new ListRow(new HeaderItem(
- getString(R.string.dvr_main_series)), mSeriesAdapter);
- updateRows();
- mDvrDataManager.addRecordedProgramListener(this);
- mDvrDataManager.addScheduledRecordingListener(this);
- mDvrDataManager.addSeriesRecordingListener(this);
- setAdapter(mRowsAdapter);
+ private boolean startBrowseIfDvrInitialized() {
+ if (mDvrDataManager.isInitialized()) {
+ // Setup rows
+ mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
+ mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
+ mSeriesAdapter = new SeriesAdapter();
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ mGenreAdapters[i] = new RecordedProgramAdapter();
+ }
+ // Schedule Recordings.
+ List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
+ onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
+ mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
+ // Recorded Programs.
+ for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
+ handleRecordedProgramAdded(recordedProgram, false);
+ }
+ // Series Recordings. Series recordings should be added after recorded programs, because
+ // we build series recordings' latest program information while adding recorded programs.
+ List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
+ handleSeriesRecordingsAdded(recordings);
+ mRecentRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_recent)), mRecentAdapter);
+ mScheduledRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_scheduled)), mScheduleAdapter);
+ mSeriesRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_series)), mSeriesAdapter);
+ mRowsAdapter.add(mScheduledRow);
+ updateRows();
+ // Initialize listeners
+ mDvrDataManager.addRecordedProgramListener(this);
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
+ startEntranceTransition();
+ return true;
+ }
+ return false;
}
private void handleRecordedProgramAdded(RecordedProgram recordedProgram,
@@ -589,10 +652,11 @@ public class DvrBrowseFragment extends BrowseFragment implements
@Override
public long getId(Object item) {
+ // We takes the inverse number for the ID of recorded programs to make the ID stable.
if (item instanceof SeriesRecording) {
return ((SeriesRecording) item).getId();
} else if (item instanceof RecordedProgram) {
- return ((RecordedProgram) item).getId();
+ return -((RecordedProgram) item).getId() - 1;
} else {
return -1;
}
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
index 806c775c..35d21db8 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
@@ -14,19 +14,23 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.app.Activity;
import android.os.Bundle;
import android.support.v17.leanback.app.DetailsFragment;
+import android.transition.Transition;
+import android.transition.Transition.TransitionListener;
+import android.view.View;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.dialog.PinDialogFragment;
/**
* Activity to show details view in DVR.
*/
-public class DvrDetailsActivity extends Activity {
+public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener {
/**
* Name of record id added to the Intent.
*/
@@ -68,6 +72,8 @@ public class DvrDetailsActivity extends Activity {
*/
public static final int SERIES_RECORDING_VIEW = 4;
+ private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener;
+
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
@@ -94,5 +100,55 @@ public class DvrDetailsActivity extends Activity {
getFragmentManager().beginTransaction()
.replace(R.id.dvr_details_view_frame, detailsFragment).commit();
}
+
+ // This is a workaround for the focus on O device
+ addTransitionListener();
+ }
+
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ if (mOnPinCheckedListener != null) {
+ mOnPinCheckedListener.onPinChecked(checked, type, rating);
+ }
+ }
+
+ void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) {
+ mOnPinCheckedListener = listener;
+ }
+
+ private void addTransitionListener() {
+ getWindow()
+ .getSharedElementEnterTransition()
+ .addListener(
+ new TransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ View actions = findViewById(R.id.details_overview_actions);
+ if (actions != null) {
+ actions.requestFocus();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ // Do nothing
+
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ // Do nothing
+ }
+ });
}
}
diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
index 21f9c4b4..19fb7117 100644
--- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -14,16 +14,14 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
-import android.content.Intent;
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;
@@ -36,20 +34,18 @@ 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.common.SoftPreconditions;
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.dialog.PinDialogFragment.OnPinCheckedListener;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.ToastUtils;
@@ -163,26 +159,6 @@ abstract class DvrDetailsFragment extends DetailsFragment {
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) {
@@ -233,10 +209,11 @@ abstract class DvrDetailsFragment extends DetailsFragment {
Toast.LENGTH_SHORT);
return;
}
+ long programId = recordedProgram.getId();
ParentalControlSettings parental = TvApplication.getSingletons(getActivity())
.getTvInputManagerHelper().getParentalControlSettings();
if (!parental.isParentalControlsEnabled()) {
- launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ DvrUiHelper.startPlaybackActivity(getContext(), programId, seekTimeMs, false);
return;
}
ChannelDataManager channelDataManager =
@@ -246,21 +223,12 @@ abstract class DvrDetailsFragment extends DetailsFragment {
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);
+ TvContentRating[] ratings = recordedProgram.getContentRatings();
+ TvContentRating blockRatings = parental.getBlockedRating(ratings);
if (blockRatings != null) {
checkPinToPlay(recordedProgram, seekTimeMs);
} else {
- launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ DvrUiHelper.startPlaybackActivity(getContext(), programId, seekTimeMs, false);
}
}
@@ -279,26 +247,21 @@ abstract class DvrDetailsFragment extends DetailsFragment {
}
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);
- }
+ SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity);
+ if (getActivity() instanceof DvrDetailsActivity) {
+ ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(new OnPinCheckedListener() {
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(null);
+ if (checked && type == PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) {
+ DvrUiHelper.startPlaybackActivity(getContext(), recordedProgram.getId(),
+ 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);
+ }
+ });
+ PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM)
+ .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
}
- intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked);
- getActivity().startActivity(intent);
}
private static class MyImageLoaderCallback extends
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..df0e61c1
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.annotation.CallSuper;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import com.android.tv.common.SoftPreconditions;
+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<T> extends Presenter {
+ protected final Context mContext;
+ private final Set<DvrItemViewHolder> mBoundViewHolders = new HashSet<>();
+ private final OnClickListener mOnClickListener = onCreateOnClickListener();
+
+ protected class DvrItemViewHolder extends ViewHolder {
+ DvrItemViewHolder(RecordingCardView view) {
+ super(view);
+ }
+
+ protected RecordingCardView getView() {
+ return (RecordingCardView) view;
+ }
+
+ protected void onBound(T item) { }
+
+ protected void onUnbound() { }
+ }
+
+ DvrItemPresenter(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent) {
+ return onCreateDvrItemViewHolder();
+ }
+
+ @Override
+ public final void onBindViewHolder(ViewHolder baseHolder, Object item) {
+ DvrItemViewHolder viewHolder;
+ T dvrItem;
+ try {
+ viewHolder = (DvrItemViewHolder) baseHolder;
+ Class<T> itemType = (Class<T>) item.getClass();
+ dvrItem = itemType.cast(item);
+ } catch (ClassCastException e) {
+ SoftPreconditions.checkState(false);
+ return;
+ }
+ viewHolder.view.setTag(item);
+ viewHolder.view.setOnClickListener(mOnClickListener);
+ onBindDvrItemViewHolder(viewHolder, dvrItem);
+ viewHolder.onBound(dvrItem);
+ mBoundViewHolders.add(viewHolder);
+ }
+
+ @Override
+ @CallSuper
+ public void onUnbindViewHolder(ViewHolder baseHolder) {
+ DvrItemViewHolder viewHolder = (DvrItemViewHolder) baseHolder;
+ mBoundViewHolders.remove(viewHolder);
+ viewHolder.onUnbound();
+ 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);
+ }
+ }
+
+ /**
+ * This method will be called when a {@link DvrItemViewHolder} is needed to be created.
+ */
+ abstract protected DvrItemViewHolder onCreateDvrItemViewHolder();
+
+ /**
+ * This method will be called when a {@link DvrItemViewHolder} is bound to a DVR item.
+ */
+ abstract protected void onBindDvrItemViewHolder(DvrItemViewHolder viewHolder, T item);
+
+ /**
+ * Returns context.
+ */
+ protected Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * 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/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
index d4d4d8ab..311137a9 100644
--- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java
+++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
/**
* Special object for schedule preview;
diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
index 7dd85f45..94c67eec 100644
--- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
@@ -14,17 +14,17 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.Context;
-import android.support.v17.leanback.widget.Presenter;
+import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.Utils;
import java.util.Collections;
@@ -33,23 +33,28 @@ import java.util.List;
/**
* Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
*/
-public class FullSchedulesCardPresenter extends Presenter {
+class FullSchedulesCardPresenter extends DvrItemPresenter<Object> {
+ private final Drawable mIconDrawable;
+ private final String mCardTitleText;
+
+ FullSchedulesCardPresenter(Context context) {
+ super(context);
+ mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule);
+ mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title);
+ }
+
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent) {
- Context context = parent.getContext();
- RecordingCardView view = new RecordingCardView(context);
- return new ScheduledRecordingViewHolder(view);
+ public DvrItemViewHolder onCreateDvrItemViewHolder() {
+ return new DvrItemViewHolder(new RecordingCardView(mContext));
}
@Override
- public void onBindViewHolder(ViewHolder baseHolder, Object o) {
- final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- final Context context = viewHolder.view.getContext();
+ public void onBindDvrItemViewHolder(DvrItemViewHolder vh, Object o) {
+ final RecordingCardView cardView = (RecordingCardView) vh.view;
- cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule));
- cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title));
- List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context)
+ cardView.setTitle(mCardTitleText);
+ cardView.setImage(mIconDrawable);
+ List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext)
.getDvrDataManager().getAvailableScheduledRecordings();
int fullDays = 0;
if (!scheduledRecordings.isEmpty()) {
@@ -57,28 +62,23 @@ public class FullSchedulesCardPresenter extends Presenter {
Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR)
.getStartTimeMs()) + 1;
}
- cardView.setContent(context.getResources().getQuantityString(
+ cardView.setContent(mContext.getResources().getQuantityString(
R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null);
-
- View.OnClickListener clickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- DvrUiHelper.startSchedulesActivity(context, null);
- }
- };
- baseHolder.view.setOnClickListener(clickListener);
}
@Override
- public void onUnbindViewHolder(ViewHolder baseHolder) {
- ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
- final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
- cardView.reset();
+ public void onUnbindViewHolder(ViewHolder vh) {
+ ((RecordingCardView) vh.view).reset();
+ super.onUnbindViewHolder(vh);
}
- private static final class ScheduledRecordingViewHolder extends ViewHolder {
- ScheduledRecordingViewHolder(RecordingCardView view) {
- super(view);
- }
+ @Override
+ protected View.OnClickListener onCreateOnClickListener() {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DvrUiHelper.startSchedulesActivity(mContext, null);
+ }
+ };
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
index e698b8a2..eb9cb26c 100644
--- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.media.tv.TvInputManager;
@@ -22,18 +22,16 @@ 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;
+import com.android.tv.dvr.data.RecordedProgram;
/**
- * {@link DetailsFragment} for recorded program in DVR.
+ * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR.
*/
public class RecordedProgramDetailsFragment extends DvrDetailsFragment
implements DvrDataManager.RecordedProgramListener {
@@ -44,7 +42,6 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
private DvrWatchedPositionManager mDvrWatchedPositionManager;
private RecordedProgram mRecordedProgram;
- private DetailsContent mDetailsContent;
private boolean mPaused;
private DvrDataManager mDvrDataManager;
@@ -59,7 +56,8 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
public void onCreateInternal() {
mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
.getDvrWatchedPositionManager();
- setDetailsOverviewRow(mDetailsContent);
+ setDetailsOverviewRow(DetailsContent
+ .createFromRecordedProgram(getContext(), mRecordedProgram));
}
@Override
@@ -87,26 +85,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment
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();
+ return mRecordedProgram != null;
}
@Override
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..5fe162b6
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
@@ -0,0 +1,142 @@
+/*
+ * 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.TvInputManager;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+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<RecordedProgram> {
+ private final DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private String mTodayString;
+ private String mYesterdayString;
+ private final int mProgressBarColor;
+ private final boolean mShowEpisodeTitle;
+ private final boolean mExpandTitleWhenFocused;
+
+ protected final class RecordedProgramViewHolder extends DvrItemViewHolder
+ implements WatchedPositionChangedListener {
+ private RecordedProgram mProgram;
+ private boolean mShowProgress;
+
+ public RecordedProgramViewHolder(RecordingCardView view, Integer progressColor) {
+ super(view);
+ if (progressColor == null) {
+ mShowProgress = false;
+ } else {
+ mShowProgress = true;
+ view.setProgressBarColor(progressColor);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ @Override
+ protected void onBound(RecordedProgram program) {
+ mProgram = program;
+ if (mShowProgress) {
+ mDvrWatchedPositionManager.addListener(this, program.getId());
+ setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId()));
+ } else {
+ getView().setProgressBar(null);
+ }
+ }
+
+ @Override
+ protected void onUnbound() {
+ if (mShowProgress) {
+ mDvrWatchedPositionManager.removeListener(this, mProgram.getId());
+ }
+ getView().reset();
+ }
+ }
+
+ RecordedProgramPresenter(Context context, boolean showEpisodeTitle,
+ boolean expandTitleWhenFocused) {
+ super(context);
+ 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 DvrItemViewHolder onCreateDvrItemViewHolder() {
+ return new RecordedProgramViewHolder(
+ new RecordingCardView(mContext, mExpandTitleWhenFocused), mProgressBarColor);
+ }
+
+ @Override
+ public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, RecordedProgram program) {
+ final RecordedProgramViewHolder viewHolder = (RecordedProgramViewHolder) baseHolder;
+ final RecordingCardView cardView = viewHolder.getView();
+ DetailsContent details = DetailsContent.createFromRecordedProgram(mContext, program);
+ cardView.setTitle(mShowEpisodeTitle ?
+ program.getEpisodeDisplayTitle(mContext) : details.getTitle());
+ cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
+ cardView.setContent(generateMajorContent(program), generateMinorContent(program));
+ cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
+ }
+
+ private String generateMajorContent(RecordedProgram program) {
+ int dateDifference = Utils.computeDateDifference(program.getStartTimeUtcMillis(),
+ System.currentTimeMillis());
+ if (dateDifference == 0) {
+ return mTodayString;
+ } else if (dateDifference == 1) {
+ return mYesterdayString;
+ } else {
+ return Utils.getDurationString(mContext, program.getStartTimeUtcMillis(),
+ program.getStartTimeUtcMillis(), false, true, false, 0);
+ }
+ }
+
+ private String generateMinorContent(RecordedProgram program) {
+ int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis()));
+ return mContext.getResources().getQuantityString(
+ R.plurals.dvr_program_duration, durationMinutes, durationMinutes);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
index 51c3b03b..767addc8 100644
--- a/src/com/android/tv/dvr/ui/RecordingCardView.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -14,51 +14,70 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.BaseCardView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.tv.R;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.ViewUtils;
import com.android.tv.util.ImageLoader;
/**
- * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or
- * {@link RecordedProgram} or
- * {@link com.android.tv.dvr.SeriesRecording}.
+ * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording}
+ * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
*/
-class RecordingCardView extends BaseCardView {
+public class RecordingCardView extends BaseCardView {
+ // This value should be the same with
+ // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
+ private final static int ANIMATION_DURATION = 150;
private final ImageView mImageView;
private final int mImageWidth;
private final int mImageHeight;
private String mImageUri;
- private final TextView mTitleView;
private final TextView mMajorContentView;
private final TextView mMinorContentView;
private final ProgressBar mProgressBar;
private final View mAffiliatedIconContainer;
private final ImageView mAffiliatedIcon;
private final Drawable mDefaultImage;
+ private final FrameLayout mTitleArea;
+ private final TextView mFoldedTitleView;
+ private final TextView mExpandedTitleView;
+ private final ValueAnimator mExpandTitleAnimator;
+ private final int mFoldedTitleHeight;
+ private final int mExpandedTitleHeight;
+ private final boolean mExpandTitleWhenFocused;
+ private boolean mExpanded;
+ private String mDetailBackgroundImageUri;
- RecordingCardView(Context context) {
- this(context,
- context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width),
- context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height));
+ public RecordingCardView(Context context) {
+ this(context, false);
}
- RecordingCardView(Context context, int imageWidth, int imageHeight) {
+ public RecordingCardView(Context context, boolean expandTitleWhenFocused) {
+ this(context, context.getResources().getDimensionPixelSize(
+ R.dimen.dvr_library_card_image_layout_width), context.getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height),
+ expandTitleWhenFocused);
+ }
+
+ public RecordingCardView(Context context, int imageWidth, int imageHeight,
+ boolean expandTitleWhenFocused) {
super(context);
//TODO(dvr): move these to the layout XML.
setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
@@ -75,13 +94,81 @@ class RecordingCardView extends BaseCardView {
mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
- mTitleView = (TextView) findViewById(R.id.title);
mMajorContentView = (TextView) findViewById(R.id.content_major);
mMinorContentView = (TextView) findViewById(R.id.content_minor);
+ mTitleArea = (FrameLayout) findViewById(R.id.title_area);
+ mFoldedTitleView = (TextView) findViewById(R.id.title_one_line);
+ mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines);
+ mFoldedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height);
+ mExpandedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height);
+ mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION);
+ mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float value = (Float) valueAnimator.getAnimatedValue();
+ mExpandedTitleView.setAlpha(value);
+ mFoldedTitleView.setAlpha(1.0f - value);
+ ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight
+ + (mExpandedTitleHeight - mFoldedTitleHeight) * value));
+ }
+ });
+ mExpandTitleWhenFocused = expandTitleWhenFocused;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ // Preload the background image going to be used in detail fragments here to prevent
+ // loading and drawing background images during activity transitions.
+ if (gainFocus) {
+ if (!TextUtils.isEmpty(mDetailBackgroundImageUri)) {
+ ImageLoader.loadBitmap(getContext(), mDetailBackgroundImageUri,
+ Integer.MAX_VALUE, Integer.MAX_VALUE, null);
+ }
+ }
+ if (mExpandTitleWhenFocused) {
+ if (gainFocus) {
+ expandTitle(true, true);
+ } else {
+ expandTitle(false, true);
+ }
+ }
+ }
+
+ /**
+ * Expands/folds the title area to show program title with two/one lines.
+ *
+ * @param expand {@code true} to expand the title area, or {@code false} to fold it.
+ * @param withAnimation {@code true} to expand/fold with animation.
+ */
+ public void expandTitle(boolean expand, boolean withAnimation) {
+ if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) {
+ if (withAnimation) {
+ if (expand) {
+ mExpandTitleAnimator.start();
+ } else {
+ mExpandTitleAnimator.reverse();
+ }
+ } else {
+ if (expand) {
+ mFoldedTitleView.setAlpha(0.0f);
+ mExpandedTitleView.setAlpha(1.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight);
+ } else {
+ mFoldedTitleView.setAlpha(1.0f);
+ mExpandedTitleView.setAlpha(0.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight);
+ }
+ }
+ mExpanded = expand;
+ }
}
void setTitle(CharSequence title) {
- mTitleView.setText(title);
+ mFoldedTitleView.setText(title);
+ mExpandedTitleView.setText(title);
}
void setContent(CharSequence majorContent, CharSequence minorContent) {
@@ -118,6 +205,11 @@ class RecordingCardView extends BaseCardView {
mProgressBar.getProgressDrawable().setTint(color);
}
+ /**
+ * Sets the image URI of the poster should be shown on the card view.
+
+ * @param isChannelLogo {@code true} if the image is from channels' logo.
+ */
void setImageUri(String uri, boolean isChannelLogo) {
if (isChannelLogo) {
mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
@@ -134,7 +226,7 @@ class RecordingCardView extends BaseCardView {
}
/**
- * Set image to card view.
+ * Sets the {@link Drawable} of the poster should be shown on the card view.
*/
public void setImage(Drawable image) {
if (image != null) {
@@ -142,6 +234,10 @@ class RecordingCardView extends BaseCardView {
}
}
+ /**
+ * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner
+ * of the poster.
+ */
public void setAffiliatedIcon(int imageResId) {
if (imageResId > 0) {
mAffiliatedIconContainer.setVisibility(View.VISIBLE);
@@ -152,6 +248,14 @@ class RecordingCardView extends BaseCardView {
}
/**
+ * Sets the background image URI of the card view, which will be displayed as background when
+ * the view is clicked and shows its details fragment.
+ */
+ public void setDetailBackgroundImageUri(String uri) {
+ mDetailBackgroundImageUri = uri;
+ }
+
+ /**
* Returns image view.
*/
public ImageView getImageView() {
@@ -178,8 +282,9 @@ class RecordingCardView extends BaseCardView {
}
public void reset() {
- mTitleView.setText(null);
+ mFoldedTitleView.setText(null);
+ mExpandedTitleView.setText(null);
setContent(null, null);
mImageView.setImageDrawable(mDefaultImage);
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
new file mode 100644
index 00000000..56ec357f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import android.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+
+import com.android.tv.TvApplication;
+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(DetailsContent
+ .createFromScheduledRecording(getContext(), mRecording));
+ }
+
+ @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;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 60816bb5..958f8bf8 100644
--- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.os.Bundle;
@@ -26,7 +26,7 @@ import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ui.DvrUiHelper;
/**
* {@link RecordingDetailsFragment} for scheduled recording in DVR.
@@ -66,7 +66,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
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.getString(R.string.dvr_detail_cancel_recording), null,
res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
return adapter;
}
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..273d3d19
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+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<ScheduledRecording> {
+ private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
+
+ private final DvrManager mDvrManager;
+ private final int mProgressBarColor;
+
+ private final class ScheduledRecordingViewHolder extends DvrItemViewHolder {
+ 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);
+ }
+
+ @Override
+ protected void onBound(ScheduledRecording recording) {
+ mScheduledRecording = recording;
+ updateProgressBar();
+ startUpdateProgressBar();
+ }
+
+ @Override
+ protected void onUnbound() {
+ stopUpdateProgressBar();
+ mScheduledRecording = null;
+ getView().reset();
+ }
+
+ 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) {
+ super(context);
+ mDvrManager = TvApplication.getSingletons(mContext).getDvrManager();
+ mProgressBarColor = mContext.getResources()
+ .getColor(R.color.play_controls_recording_icon_color_on_focus);
+ }
+
+ @Override
+ public DvrItemViewHolder onCreateDvrItemViewHolder() {
+ return new ScheduledRecordingViewHolder(new RecordingCardView(mContext), mProgressBarColor);
+ }
+
+ @Override
+ public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder,
+ ScheduledRecording recording) {
+ final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final RecordingCardView cardView = viewHolder.getView();
+ DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording);
+ cardView.setTitle(details.getTitle());
+ cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
+ cardView.setAffiliatedIcon(mDvrManager.isConflicting(recording) ?
+ R.drawable.ic_warning_white_32dp : 0);
+ cardView.setContent(generateMajorContent(recording), null);
+ cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
+ }
+
+ private String generateMajorContent(ScheduledRecording recording) {
+ int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(),
+ recording.getStartTimeMs());
+ if (dateDifference <= 0) {
+ return mContext.getString(R.string.dvr_date_today_time,
+ Utils.getDurationString(mContext, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0));
+ } else if (dateDifference == 1) {
+ return mContext.getString(R.string.dvr_date_tomorrow_time,
+ Utils.getDurationString(mContext, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0));
+ } else {
+ return Utils.getDurationString(mContext, recording.getStartTimeMs(),
+ recording.getStartTimeMs(), false, true, false, 0);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
index e9e391d4..c2aa8e98 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
@@ -28,7 +28,6 @@ import android.support.v17.leanback.widget.DetailsOverviewRow;
import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
import android.support.v17.leanback.widget.HeaderItem;
import android.support.v17.leanback.widget.ListRow;
-import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.PresenterSelector;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
@@ -37,14 +36,12 @@ 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 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;
@@ -67,7 +64,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
// After fragments are created, it should be cleared to save resources.
private List<RecordedProgram> mRecordedPrograms;
private RecordedProgram mRecommendRecordedProgram;
- private DetailsContent mDetailsContent;
private int mSeasonRowCount;
private SparseArrayObjectAdapter mActionsAdapter;
private Action mDeleteAction;
@@ -85,7 +81,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
mWatchLabel = getString(R.string.dvr_detail_watch);
mResumeLabel = getString(R.string.dvr_detail_series_resume);
mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null);
- mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true);
+ mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true);
super.onCreate(savedInstanceState);
}
@@ -93,7 +89,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
protected void onCreateInternal() {
mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
.getDvrWatchedPositionManager();
- setDetailsOverviewRow(mDetailsContent);
+ setDetailsOverviewRow(DetailsContent.createFromSeriesRecording(getContext(), mSeries));
setupRecordedProgramsRow();
mDvrDataManager.addSeriesRecordingListener(this);
mDvrDataManager.addRecordedProgramListener(this);
@@ -149,7 +145,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
}
mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId());
Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR);
- mDetailsContent = createDetailsContent();
return true;
}
@@ -158,22 +153,10 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
DetailsOverviewRowPresenter rowPresenter) {
ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
- presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());
+ presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
return presenterSelector;
}
- 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());
@@ -203,10 +186,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
mDvrDataManager.removeSeriesRecordingListener(this);
mDvrDataManager.removeRecordedProgramListener(this);
if (mSeries != null) {
- DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
- if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) {
- dvrManager.removeSeriesRecording(mSeries.getId());
- }
+ mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId());
}
mRecordedProgramPresenter.unbindAllViewHolders();
}
@@ -265,7 +245,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
for (SeriesRecording series : seriesRecordings) {
if (series.getId() == mSeries.getId()) {
- mSeries = null;
getActivity().finish();
return;
}
@@ -372,4 +351,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement
return program.getId();
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
index c2c0f596..e508259d 100644
--- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
@@ -14,42 +14,36 @@
* limitations under the License.
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.browse;
-import android.app.Activity;
import android.content.Context;
-import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
import android.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 com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import java.util.List;
/**
* Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}.
*/
-public class SeriesRecordingPresenter extends DvrItemPresenter {
- private final ChannelDataManager mChannelDataManager;
+class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> {
private final DvrDataManager mDvrDataManager;
private final DvrManager mDvrManager;
private final DvrWatchedPositionManager mWatchedPositionManager;
- private static final class SeriesRecordingViewHolder extends ViewHolder implements
+ private final class SeriesRecordingViewHolder extends DvrItemViewHolder implements
WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener {
private SeriesRecording mSeriesRecording;
private RecordingCardView mCardView;
@@ -138,7 +132,8 @@ public class SeriesRecordingPresenter extends DvrItemPresenter {
// Do nothing
}
- public void onBound(SeriesRecording seriesRecording) {
+ @Override
+ protected void onBound(SeriesRecording seriesRecording) {
mSeriesRecording = seriesRecording;
mDvrDataManager.addScheduledRecordingListener(this);
mDvrDataManager.addRecordedProgramListener(this);
@@ -152,10 +147,12 @@ public class SeriesRecordingPresenter extends DvrItemPresenter {
updateCardViewContent();
}
- public void onUnbound() {
+ @Override
+ protected void onUnbound() {
mDvrDataManager.removeScheduledRecordingListener(this);
mDvrDataManager.removeRecordedProgramListener(this);
mWatchedPositionManager.removeListener(this);
+ getView().reset();
}
private void updateCardViewContent() {
@@ -186,29 +183,28 @@ public class SeriesRecordingPresenter extends DvrItemPresenter {
}
public SeriesRecordingPresenter(Context context) {
+ super(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);
+ public DvrItemViewHolder onCreateDvrItemViewHolder() {
+ return new SeriesRecordingViewHolder(new RecordingCardView(mContext), mDvrDataManager,
+ mDvrManager, mWatchedPositionManager);
}
@Override
- public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, SeriesRecording series) {
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);
+ final RecordingCardView cardView = viewHolder.getView();
+ viewHolder.onBound(series);
+ DetailsContent details = DetailsContent.createFromSeriesRecording(mContext, series);
+ cardView.setTitle(details.getTitle());
+ cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
+ cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
}
@Override
@@ -217,18 +213,4 @@ public class SeriesRecordingPresenter extends DvrItemPresenter {
((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..b9407b15 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.
@@ -40,7 +40,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment
/**
* The key for scheduled recording which has be selected in the list.
*/
- public static String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording";
+ public static final String SCHEDULES_KEY_SCHEDULED_RECORDING =
+ "schedules_key_scheduled_recording";
private ScheduleRowAdapter mRowsAdapter;
private TextView mEmptyInfoScreenView;
diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
index f6e6ac26..a0410bb3 100644
--- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.list;
import android.app.Activity;
import android.app.ProgressDialog;
@@ -24,15 +24,13 @@ import android.support.annotation.IntDef;
import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.Program;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.SeriesRecordingScheduler;
-import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
-import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
+import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
+import com.android.tv.dvr.ui.BigArguments;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -72,33 +70,47 @@ public class DvrSchedulesActivity extends Activity {
getFragmentManager().beginTransaction().add(
R.id.fragment_container, schedulesFragment).commit();
} else if (scheduleType == TYPE_SERIES_SCHEDULE) {
- final ProgressDialog dialog = ProgressDialog.show(this, null, getString(
- R.string.dvr_series_schedules_progress_message_reading_programs));
- SeriesRecording seriesRecording = getIntent().getExtras()
- .getParcelable(DvrSeriesSchedulesFragment
- .SERIES_SCHEDULES_KEY_SERIES_RECORDING);
- // To get programs faster, hold the update of the series schedules.
- SeriesRecordingScheduler.getInstance(this).pauseUpdate();
- new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) {
- @Override
- protected void onPostExecute(List<Program> programs) {
- SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate();
- dialog.dismiss();
- Bundle args = getIntent().getExtras();
- args.putParcelableArrayList(DvrSeriesSchedulesFragment
- .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs));
- DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment();
- schedulesFragment.setArguments(args);
- getFragmentManager().beginTransaction().add(
- R.id.fragment_container, schedulesFragment).commit();
- }
- }.setLoadCurrentProgram(true)
- .setLoadDisallowedProgram(true)
- .setLoadScheduledEpisode(true)
- .setIgnoreChannelOption(true)
- .execute();
+ if (BigArguments.getArgument(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) {
+ // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need
+ // to reset the BigArguments.
+ showDvrSeriesSchedulesFragment(getIntent().getExtras());
+ } else {
+ final ProgressDialog dialog = ProgressDialog.show(this, null, getString(
+ R.string.dvr_series_progress_message_reading_programs));
+ SeriesRecording seriesRecording = getIntent().getExtras()
+ .getParcelable(DvrSeriesSchedulesFragment
+ .SERIES_SCHEDULES_KEY_SERIES_RECORDING);
+ // To get programs faster, hold the update of the series schedules.
+ SeriesRecordingScheduler.getInstance(this).pauseUpdate();
+ new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) {
+ @Override
+ protected void onPostExecute(List<Program> programs) {
+ SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this)
+ .resumeUpdate();
+ dialog.dismiss();
+ Bundle args = getIntent().getExtras();
+ BigArguments.reset();
+ BigArguments.setArgument(
+ DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS,
+ programs == null ? Collections.EMPTY_LIST : programs);
+ showDvrSeriesSchedulesFragment(args);
+ }
+ }.setLoadCurrentProgram(true)
+ .setLoadDisallowedProgram(true)
+ .setLoadScheduledEpisode(true)
+ .setIgnoreChannelOption(true)
+ .execute();
+ }
} else {
finish();
}
}
+
+ private void showDvrSeriesSchedulesFragment(Bundle args) {
+ DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment();
+ schedulesFragment.setArguments(args);
+ getFragmentManager().beginTransaction().add(
+ R.id.fragment_container, schedulesFragment).commit();
+ }
}
diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
index 722c9b6e..3cbb500a 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java
@@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list;
import android.os.Bundle;
import android.support.v17.leanback.widget.ClassPresenterSelector;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
import com.android.tv.R;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
/**
diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
index 42a1e72b..57e7a88f 100644
--- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java
@@ -17,6 +17,7 @@
package com.android.tv.dvr.ui.list;
import android.annotation.TargetApi;
+import android.content.Context;
import android.database.ContentObserver;
import android.media.tv.TvContract.Programs;
import android.net.Uri;
@@ -35,11 +36,13 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
+import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
-import com.android.tv.dvr.EpisodicProgramLoadTask;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
+import com.android.tv.dvr.ui.BigArguments;
+import java.util.Collections;
import java.util.List;
/**
@@ -47,20 +50,22 @@ import java.util.List;
*/
@TargetApi(Build.VERSION_CODES.N)
public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
- private static final String TAG = "DvrSeriesSchedulesFragment";
/**
* The key for series recording whose scheduled recording list will be displayed.
+ * Type: {@link SeriesRecording}
*/
public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING =
"series_schedules_key_series_recording";
/**
- * The key for programs belong to the series recording whose scheduled recording
- * list will be displayed.
+ * The key for programs which belong to the series recording whose scheduled recording list
+ * will be displayed.
+ * Type: List<{@link Program}>
*/
public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS =
"series_schedules_key_series_programs";
private ChannelDataManager mChannelDataManager;
+ private DvrDataManager mDvrDataManager;
private SeriesRecording mSeriesRecording;
private List<Program> mPrograms;
private EpisodicProgramLoadTask mProgramLoadTask;
@@ -87,20 +92,22 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
&& getRowsAdapter() instanceof SeriesScheduleRowAdapter) {
((SeriesScheduleRowAdapter) getRowsAdapter())
.onSeriesRecordingUpdated(r);
+ mSeriesRecording = r;
+ updateEmptyMessage();
return;
}
}
}
};
- private final ContentObserver mContentObserver =
- new ContentObserver(new Handler(Looper.getMainLooper())) {
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- super.onChange(selfChange, uri);
- executeProgramLoadingTask();
- }
- };
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private final ContentObserver mContentObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ super.onChange(selfChange, uri);
+ executeProgramLoadingTask();
+ }
+ };
private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() {
@Override
@@ -120,17 +127,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
}
@Override
- public void onCreate(Bundle savedInstanceState) {
+ public void onAttach(Context context) {
+ super.onAttach(context);
Bundle args = getArguments();
if (args != null) {
mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING);
- mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS);
+ mPrograms = (List<Program>) BigArguments.getArgument(
+ SERIES_SCHEDULES_KEY_SERIES_PROGRAMS);
+ BigArguments.reset();
}
+ if (args == null || mPrograms == null) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ApplicationSingletons singletons = TvApplication.getSingletons(getContext());
- singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener);
mChannelDataManager = singletons.getChannelDataManager();
mChannelDataManager.addListener(mChannelListener);
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener);
getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
mContentObserver);
}
@@ -144,8 +162,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
private void onProgramsUpdated() {
((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms);
+ updateEmptyMessage();
+ }
+
+ private void updateEmptyMessage() {
if (mPrograms == null || mPrograms.isEmpty()) {
- showEmptyMessage(R.string.dvr_series_schedules_empty_state);
+ if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) {
+ showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state);
+ } else {
+ showEmptyMessage(R.string.dvr_series_schedules_empty_state);
+ }
} else {
hideEmptyMessage();
}
@@ -158,15 +184,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
mProgramLoadTask = null;
}
getContext().getContentResolver().unregisterContentObserver(mContentObserver);
+ mHandler.removeCallbacksAndMessages(null);
mChannelDataManager.removeListener(mChannelListener);
- TvApplication.getSingletons(getContext()).getDvrDataManager()
- .removeSeriesRecordingListener(mSeriesRecordingListener);
+ mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener);
super.onDestroy();
}
@Override
public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() {
- return new SeriesRecordingHeaderRowPresenter(getContext());
+ return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext());
}
@Override
@@ -195,7 +221,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) {
@Override
protected void onPostExecute(List<Program> programs) {
- mPrograms = programs;
+ mPrograms = programs == null ? Collections.EMPTY_LIST : programs;
onProgramsUpdated();
}
};
@@ -205,4 +231,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment {
.setIgnoreChannelOption(true)
.execute();
}
-} \ No newline at end of file
+}
diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
index 23aebf59..2af832ec 100644
--- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
+++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java
@@ -19,13 +19,14 @@ 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;
+import com.android.tv.dvr.ui.DvrUiHelper;
/**
* A class for the episodic program.
*/
-public class EpisodicProgramRow extends ScheduleRow {
+class EpisodicProgramRow extends ScheduleRow {
private final String mInputId;
private final Program mProgram;
@@ -65,7 +66,7 @@ public class EpisodicProgramRow extends ScheduleRow {
@Override
public String getProgramTitleWithEpisodeNumber(Context context) {
- return mProgram.getTitleWithEpisodeNumber(context);
+ return DvrUiHelper.getStyledTitleWithEpisodeNumber(context, mProgram, 0).toString();
}
@Override
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
index 3fc92e8a..91ba393a 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
@@ -20,12 +20,13 @@ 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;
+import com.android.tv.dvr.ui.DvrUiHelper;
/**
* A class for schedule recording row.
*/
-public class ScheduleRow {
+class ScheduleRow {
private final SchedulesHeaderRow mHeaderRow;
@Nullable private ScheduledRecording mSchedule;
private boolean mStopRecordingRequested;
@@ -166,7 +167,8 @@ public class ScheduleRow {
* Returns the program title with episode number.
*/
public String getProgramTitleWithEpisodeNumber(Context context) {
- return mSchedule != null ? mSchedule.getProgramTitleWithEpisodeNumber(context) : null;
+ return mSchedule != null ? DvrUiHelper.getStyledTitleWithEpisodeNumber(context,
+ mSchedule, 0).toString() : null;
}
/**
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index 9cc82653..97d60473 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -30,8 +30,8 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit;
/**
* An adapter for {@link ScheduleRow}.
*/
-public class ScheduleRowAdapter extends ArrayObjectAdapter {
+class ScheduleRowAdapter extends ArrayObjectAdapter {
private static final String TAG = "ScheduleRowAdapter";
private static final boolean DEBUG = false;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 1257e725..dc4e3c41 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -42,25 +42,24 @@ import com.android.tv.R;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* A RowPresenter for {@link ScheduleRow}.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class ScheduleRowPresenter extends RowPresenter {
+class ScheduleRowPresenter extends RowPresenter {
private static final String TAG = "ScheduleRowPresenter";
@Retention(RetentionPolicy.SOURCE)
@@ -345,7 +344,9 @@ public class ScheduleRowPresenter extends RowPresenter {
viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- onInfoClicked(row);
+ if (isInfoClickable(row)) {
+ onInfoClicked(row);
+ }
}
});
@@ -366,8 +367,7 @@ public class ScheduleRowPresenter extends RowPresenter {
viewHolder.mTimeView.setText(onGetRecordingTimeText(row));
String programInfoText = onGetProgramInfoText(row);
if (TextUtils.isEmpty(programInfoText)) {
- int durationMins =
- Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1);
+ int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration()));
programInfoText = mContext.getResources().getQuantityString(
R.plurals.dvr_schedules_recording_duration, durationMins, durationMins);
}
@@ -403,6 +403,7 @@ public class ScheduleRowPresenter extends RowPresenter {
} else {
viewHolder.whiteBackInfo();
}
+ viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
updateActionContainer(viewHolder, viewHolder.isSelected());
}
@@ -454,11 +455,13 @@ public class ScheduleRowPresenter extends RowPresenter {
/**
* Called when user click Info in {@link ScheduleRow}.
*/
- protected void onInfoClicked(ScheduleRow scheduleRow) {
- ScheduledRecording schedule = scheduleRow.getSchedule();
- if (schedule != null) {
- DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true);
- }
+ protected void onInfoClicked(ScheduleRow row) {
+ DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true);
+ }
+
+ private boolean isInfoClickable(ScheduleRow row) {
+ return row.getSchedule() != null
+ && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress());
}
/**
@@ -545,7 +548,7 @@ public class ScheduleRowPresenter extends RowPresenter {
// This row has been deleted.
return;
}
- if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
+ if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
row.setStopRecordingRequested(true);
mDvrManager.stopRecording(row.getSchedule());
CharSequence deletedInfo = onGetProgramInfoText(row);
@@ -670,10 +673,9 @@ public class ScheduleRowPresenter extends RowPresenter {
hideActionView(viewHolder.mFirstActionContainer, View.GONE);
}
};
- if (mLastFocusedViewId == R.id.action_first_container
- || mLastFocusedViewId == R.id.action_second_container) {
- mLastFocusedViewId = R.id.info_container;
- }
+ mLastFocusedViewId = R.id.info_container;
+ SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG,
+ "No focusable view in this row: " + viewHolder);
break;
}
View view = viewHolder.view.findViewById(mLastFocusedViewId);
@@ -683,8 +685,10 @@ public class ScheduleRowPresenter extends RowPresenter {
// requestFocus() explicitly.
if (view.hasFocus()) {
viewHolder.mPendingAnimationRunnable.run();
- } else {
+ } else if (view.isFocusable()){
view.requestFocus();
+ } else {
+ viewHolder.view.requestFocus();
}
}
} else {
@@ -737,10 +741,10 @@ public class ScheduleRowPresenter extends RowPresenter {
@ScheduleRowAction
protected int[] getAvailableActions(ScheduleRow row) {
if (row.getSchedule() != null) {
- if (row.isOnAir()) {
- if (row.isRecordingInProgress()) {
- return new int[] {ACTION_STOP_RECORDING};
- } else if (row.isRecordingNotStarted()) {
+ if (row.isRecordingInProgress()) {
+ return new int[]{ACTION_STOP_RECORDING};
+ } else if (row.isOnAir()) {
+ if (row.isRecordingNotStarted()) {
if (canResolveConflict()) {
// The "START" action can change the conflict states.
return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING};
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
index 0fb0924d..715ecb8c 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java
@@ -16,12 +16,15 @@
package com.android.tv.dvr.ui.list;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.data.Program;
+import com.android.tv.dvr.data.SeriesRecording;
+
+import java.util.List;
/**
* A base class for the rows for schedules' header.
*/
-public abstract class SchedulesHeaderRow {
+abstract class SchedulesHeaderRow {
private String mTitle;
private String mDescription;
private int mItemCount;
@@ -98,11 +101,20 @@ public abstract class SchedulesHeaderRow {
*/
public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow {
private SeriesRecording mSeriesRecording;
+ private List<Program> mPrograms;
public SeriesRecordingHeaderRow(String title, String description, int itemCount,
- SeriesRecording series) {
+ SeriesRecording series, List<Program> programs) {
super(title, description, itemCount);
mSeriesRecording = series;
+ mPrograms = programs;
+ }
+
+ /**
+ * Returns the list of programs which belong to the series.
+ */
+ public List<Program> getPrograms() {
+ return mPrograms;
}
/**
@@ -119,4 +131,4 @@ public abstract class SchedulesHeaderRow {
mSeriesRecording = seriesRecording;
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
index 69c33a96..fe2033ba 100644
--- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java
@@ -30,15 +30,14 @@ import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.SeriesRecording;
-import com.android.tv.dvr.ui.DvrSchedulesActivity;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
/**
* A base class for RowPresenter for {@link SchedulesHeaderRow}
*/
-public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
+abstract class SchedulesHeaderRowPresenter extends RowPresenter {
private Context mContext;
public SchedulesHeaderRowPresenter(Context context) {
@@ -79,7 +78,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
/**
- * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}.
*/
public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter {
public DateHeaderRowPresenter(Context context) {
@@ -93,7 +92,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
/**
* A ViewHolder for
- * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}.
+ * {@link SchedulesHeaderRow.DateHeaderRow}.
*/
public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder {
public DateHeaderRowViewHolder(Context context, ViewGroup parent) {
@@ -152,9 +151,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
- // TODO: pass channel list for settings.
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- header.getSeriesRecording().getId(), null, false, false, false);
+ header.getSeriesRecording().getId(),
+ header.getPrograms(), false, false, false, null);
}
});
headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() {
@@ -169,9 +168,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
.build();
TvApplication.getSingletons(getContext()).getDvrManager()
.updateSeriesRecording(seriesRecording);
- // TODO: pass channel list for settings.
DvrUiHelper.startSeriesSettingsActivity(getContext(),
- header.getSeriesRecording().getId(), null, false, false, false);
+ header.getSeriesRecording().getId(),
+ header.getPrograms(), false, false, false, null);
} else {
DvrUiHelper.showCancelAllSeriesRecordingDialog(
(DvrSchedulesActivity) view.getContext(),
@@ -182,11 +181,8 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter {
}
private void setTextDrawable(TextView textView, Drawable drawableStart) {
- if (mLtr) {
- textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null);
- } else {
- textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null);
- }
+ textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null,
+ null);
}
/**
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
index 3b493774..6b6de8b8 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java
@@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Program;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.ScheduledRecording;
-import com.android.tv.dvr.SeriesRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow;
import com.android.tv.util.Utils;
@@ -46,7 +46,7 @@ import java.util.Map;
* An adapter for series schedule row.
*/
@TargetApi(Build.VERSION_CODES.N)
-public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
+class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
private static final String TAG = "SeriesRowAdapter";
private static final boolean DEBUG = false;
@@ -96,7 +96,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
Collections.sort(sortedPrograms);
List<EpisodicProgramRow> rows = new ArrayList<>();
mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(),
- null, sortedPrograms.size(), mSeriesRecording);
+ null, sortedPrograms.size(), mSeriesRecording, programs);
for (Program program : sortedPrograms) {
ScheduledRecording schedule =
mDataManager.getScheduledRecordingForProgramId(program.getId());
@@ -145,7 +145,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
if (index != -1) {
EpisodicProgramRow row = (EpisodicProgramRow) get(index);
if (!row.isStartRecordingRequested()) {
- row.setSchedule(schedule);
+ setScheduleToRow(row, schedule);
notifyArrayItemRangeChanged(index, 1);
}
}
@@ -195,12 +195,10 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
if (!isStartOrStopRequested()) {
executePendingUpdate();
}
- row.setSchedule(schedule);
+ setScheduleToRow(row, schedule);
}
- } else if (willBeKept(schedule)) {
- row.setSchedule(schedule);
} else {
- row.setSchedule(null);
+ setScheduleToRow(row, schedule);
}
notifyArrayItemRangeChanged(index, 1);
}
@@ -213,6 +211,14 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter {
}
}
+ private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) {
+ if (schedule != null && willBeKept(schedule)) {
+ row.setSchedule(schedule);
+ } else {
+ row.setSchedule(null);
+ }
+ }
+
private int findRowIndexByProgramId(long programId) {
for (int i = 0; i < size(); i++) {
Object item = get(i);
diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
index 5d88579a..c8503e0d 100644
--- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java
@@ -22,13 +22,13 @@ import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.common.SoftPreconditions;
-import com.android.tv.dvr.DvrUiHelper;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.Utils;
/**
* A RowPresenter for series schedule row.
*/
-public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
+class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
private static final String TAG = "SeriesRowPresenter";
private boolean mLtr;
@@ -74,13 +74,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext()
.getResources().getDimensionPixelOffset(
R.dimen.dvr_schedules_warning_icon_padding));
- if (mLtr) {
- viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
- R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
- } else {
- viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(
- 0, 0, R.drawable.ic_warning_gray600_36dp, 0);
- }
+ viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.ic_warning_gray600_36dp, 0, 0, 0);
} else {
viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
@@ -88,9 +83,7 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter {
@Override
protected void onInfoClicked(ScheduleRow row) {
- if (row.getSchedule() != null) {
- DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule());
- }
+ DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule());
}
@Override
diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
index 5deda44a..6824cfe2 100644
--- a/src/com/android/tv/dvr/DvrPlaybackActivity.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java
@@ -14,32 +14,38 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
+import android.content.ContentUris;
import android.content.Intent;
import android.content.res.Configuration;
+import android.net.Uri;
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;
+import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.util.Utils;
/**
* Activity to play a {@link RecordedProgram}.
*/
-public class DvrPlaybackActivity extends Activity {
+public class DvrPlaybackActivity extends Activity implements OnPinCheckedListener {
private static final String TAG = "DvrPlaybackActivity";
private static final boolean DEBUG = false;
private DvrPlaybackOverlayFragment mOverlayFragment;
+ private OnPinCheckedListener mOnPinCheckedListener;
@Override
public void onCreate(Bundle savedInstanceState) {
TvApplication.setCurrentRunningProcess(this, true);
if (DEBUG) Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
+ setIntent(createProgramIntent(getIntent()));
setContentView(R.layout.activity_dvr_playback);
mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager()
.findFragmentById(R.id.dvr_playback_controls_fragment);
@@ -54,7 +60,8 @@ public class DvrPlaybackActivity extends Activity {
@Override
protected void onNewIntent(Intent intent) {
- mOverlayFragment.onNewIntent(intent);
+ setIntent(createProgramIntent(intent));
+ mOverlayFragment.onNewIntent(createProgramIntent(intent));
}
@Override
@@ -64,4 +71,24 @@ public class DvrPlaybackActivity extends Activity {
mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density),
(int) (newConfig.screenHeightDp * density));
}
+
+ private Intent createProgramIntent(Intent intent) {
+ if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+ Uri uri = intent.getData();
+ long recordedProgramId = ContentUris.parseId(uri);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId);
+ }
+ return intent;
+ }
+
+ @Override
+ public void onPinChecked(boolean checked, int type, String rating) {
+ if (mOnPinCheckedListener != null) {
+ mOnPinCheckedListener.onPinChecked(checked, type, rating);
+ }
+ }
+
+ void setOnPinCheckListener(OnPinCheckedListener listener) {
+ mOnPinCheckedListener = listener;
+ }
} \ 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..8ef0041d
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.tv.R;
+import com.android.tv.dvr.ui.browse.RecordedProgramPresenter;
+import com.android.tv.dvr.ui.browse.RecordingCardView;
+
+/**
+ * This class is used to generate Views and bind Objects for related recordings in DVR playback.
+ */
+class DvrPlaybackCardPresenter extends RecordedProgramPresenter {
+ 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 DvrItemViewHolder onCreateDvrItemViewHolder() {
+ return new RecordedProgramViewHolder(new RecordingCardView(
+ getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true), null);
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
index 0bc4ecb1..1a6ae187 100644
--- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java
@@ -14,66 +14,80 @@
* limitations under the License
*/
-package com.android.tv.dvr.ui;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
+import android.content.Context;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaController.TransportControls;
import android.media.session.PlaybackState;
-import android.support.v17.leanback.app.PlaybackControlGlue;
+import android.media.tv.TvTrackInfo;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.media.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.ArrayObjectAdapter;
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.
*/
-public class DvrPlaybackControlHelper extends PlaybackControlGlue {
- private static final String TAG = "DvrPlaybackControlHelper";
+class DvrPlaybackControlHelper extends PlaybackControlGlue {
+ private static final String TAG = "DvrPlaybackControlHelpr";
private static final boolean DEBUG = false;
- /**
- * Indicates the ID of the media under playback is unknown.
- */
- public static int UNKNOWN_MEDIA_ID = -1;
+ private static final int AUDIO_ACTION_ID = 1001;
private int mPlaybackState = PlaybackState.STATE_NONE;
private int mPlaybackSpeedLevel;
private int mPlaybackSpeedId;
private boolean mReadyToControl;
+ private final DvrPlaybackOverlayFragment mFragment;
private final MediaController mMediaController;
private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
private final TransportControls mTransportControls;
private final int mExtraPaddingTopForNoDescription;
+ private final MultiAction mClosedCaptioningAction;
+ private final MultiAction mMultiAudioAction;
+ private ArrayObjectAdapter mSecondaryActionsAdapter;
- public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
- super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
+ DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) {
+ super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]);
+ mFragment = overlayFragment;
mMediaController = activity.getMediaController();
mMediaController.registerCallback(mMediaControllerCallback);
mTransportControls = mMediaController.getTransportControls();
mExtraPaddingTopForNoDescription = activity.getResources()
.getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top);
+ mClosedCaptioningAction = new ClosedCaptioningAction(activity);
+ mMultiAudioAction = new MultiAudioAction(activity);
+ createControlsRowPresenter();
}
- @Override
- public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
+ void createControlsRow() {
PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
setControlsRow(controlsRow);
+ mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter();
+ }
+
+ private void createControlsRowPresenter() {
AbstractDetailsDescriptionPresenter detailsPresenter =
new AbstractDetailsDescriptionPresenter() {
@Override
@@ -112,30 +126,31 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
.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;
+ setControlsRowPresenter(presenter);
}
@Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
+ public void onActionClicked(Action action) {
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));
+ int trackType;
+ if (action.getId() == mClosedCaptioningAction.getId()) {
+ trackType = TvTrackInfo.TYPE_SUBTITLE;
+ } else if (action.getId() == AUDIO_ACTION_ID) {
+ trackType = TvTrackInfo.TYPE_AUDIO;
+ } else {
+ super.onActionClicked(action);
+ return;
+ }
+ ArrayList<TvTrackInfo> trackInfos = mFragment.getTracks(trackType);
+ if (!trackInfos.isEmpty()) {
+ showSideFragment(trackInfos, mFragment.getSelectedTrackId(trackType));
}
- return super.onKey(v, keyCode, event);
}
- return false;
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ return mReadyToControl && super.onKey(v, keyCode, event);
}
@Override
@@ -158,10 +173,10 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
/**
* Returns the ID of the media under playback.
*/
- public long getMediaId() {
+ public String getMediaId() {
MediaMetadata mediaMetadata = mMediaController.getMetadata();
- return mediaMetadata == null ? UNKNOWN_MEDIA_ID
- : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID);
+ return mediaMetadata == null ? null
+ : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
}
@Override
@@ -213,12 +228,45 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
/**
* Unregister media controller's callback.
*/
- public void unregisterCallback() {
+ 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.
+ */
+ 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);
+ }
+ getHost().notifyPlaybackRowChanged();
+ }
+
+ @Nullable
+ Boolean hasSecondaryRow() {
+ if (mSecondaryActionsAdapter == null) {
+ return null;
+ }
+ return mSecondaryActionsAdapter.size() != 0;
+ }
+
@Override
- protected void startPlayback(int speedId) {
+ public void play(int speedId) {
if (getCurrentSpeedId() == speedId) {
return;
}
@@ -232,23 +280,16 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
}
@Override
- protected void pausePlayback() {
+ public void pause() {
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) {
@@ -297,6 +338,19 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
onStateChanged();
}
+ private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) {
+ Bundle args = new Bundle();
+ args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos);
+ args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId);
+ DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment();
+ sideFragment.setArguments(args);
+ mFragment.getFragmentManager().beginTransaction()
+ .hide(mFragment)
+ .replace(R.id.dvr_playback_side_fragment, sideFragment)
+ .addToBackStack(null)
+ .commit();
+ }
+
private class MediaControllerCallback extends MediaController.Callback {
@Override
public void onPlaybackStateChanged(PlaybackState state) {
@@ -307,7 +361,13 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue {
@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/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
index 9759a856..843d2dbe 100644
--- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java
@@ -14,7 +14,7 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.app.Activity;
import android.content.Intent;
@@ -31,14 +31,16 @@ import android.text.TextUtils;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TimeShiftUtils;
import com.android.tv.util.Utils;
-public class DvrPlaybackMediaSessionHelper {
+class DvrPlaybackMediaSessionHelper {
private static final String TAG = "DvrPlaybackMediaSessionHelper";
private static final boolean DEBUG = false;
@@ -102,6 +104,7 @@ public class DvrPlaybackMediaSessionHelper {
}
if (mMediaSession != null) {
mMediaSession.release();
+ mMediaSession = null;
}
}
@@ -179,83 +182,88 @@ public class DvrPlaybackMediaSessionHelper {
cardTitleText = (channel != null) ? channel.getDisplayName()
: mActivity.getString(R.string.no_program_information);
}
- updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(),
- mProgramDurationMs, null, 0);
+ final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText,
+ program.getDescription(), mProgramDurationMs);
String posterArtUri = program.getPosterArtUri();
if (posterArtUri == null) {
posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString();
}
- updatePosterArt(program, cardTitleText, program.getDescription(),
- mProgramDurationMs, null, posterArtUri);
+ updatePosterArt(program, currentMetadata, null, posterArtUri);
mMediaSession.setActive(true);
}
- private void updatePosterArt(RecordedProgram program, String cardTitleText,
- String cardSubtitleText, long duration,
+ private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata,
@Nullable Bitmap posterArt, @Nullable String posterArtUri) {
if (posterArt != null) {
- updateMediaMetadata(program.getId(), cardTitleText,
- cardSubtitleText, duration, posterArt, 0);
+ updateMetadataImageInfo(program, currentMetadata, posterArt, 0);
} else if (posterArtUri != null) {
ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth,
- mNowPlayingCardHeight, new ProgramPosterArtCallback(
- mActivity, program, cardTitleText, cardSubtitleText, duration));
+ mNowPlayingCardHeight,
+ new ProgramPosterArtCallback(mActivity, program, currentMetadata));
} else {
- updateMediaMetadata(program.getId(), cardTitleText,
- cardSubtitleText, duration, null, R.drawable.default_now_card);
+ updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card);
}
}
private class ProgramPosterArtCallback extends
ImageLoader.ImageLoaderCallback<Activity> {
- private RecordedProgram mRecordedProgram;
- private String mCardTitleText;
- private String mCardSubtitleText;
- private long mDuration;
+ private final RecordedProgram mRecordedProgram;
+ private final MediaMetadata mCurrentMetadata;
public ProgramPosterArtCallback(Activity activity, RecordedProgram program,
- String cardTitleText, String cardSubtitleText, long duration) {
+ MediaMetadata metadata) {
super(activity);
mRecordedProgram = program;
- mCardTitleText = cardTitleText;
- mCardSubtitleText = cardSubtitleText;
- mDuration = duration;
+ mCurrentMetadata = metadata;
}
@Override
public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) {
if (isCurrentProgram(mRecordedProgram)) {
- updatePosterArt(mRecordedProgram, mCardTitleText,
- mCardSubtitleText, mDuration, posterArt, null);
+ updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null);
}
}
}
- private void updateMediaMetadata(final long programId, final String title,
- final String subtitle, final long duration,
- final Bitmap posterArt, final int imageResId) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... arg0) {
- MediaMetadata.Builder builder = new MediaMetadata.Builder();
- builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId)
- .putString(MediaMetadata.METADATA_KEY_TITLE, title)
- .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
- if (subtitle != null) {
- builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
- }
- Bitmap programPosterArt = posterArt;
- if (programPosterArt == null && imageResId != 0) {
- programPosterArt =
- BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
- }
- if (programPosterArt != null) {
- builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
- }
+ private MediaMetadata updateMetadataTextInfo(final long programId, final String title,
+ final String subtitle, final long duration) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId))
+ .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+ .putLong(MediaMetadata.METADATA_KEY_DURATION, duration);
+ if (subtitle != null) {
+ builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle);
+ }
+ MediaMetadata metadata = builder.build();
+ mMediaSession.setMetadata(metadata);
+ return metadata;
+ }
+
+ private void updateMetadataImageInfo(final RecordedProgram program,
+ final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) {
+ if (mMediaSession != null && (posterArt != null || imageResId != 0)) {
+ MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata);
+ if (posterArt != null) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt);
mMediaSession.setMetadata(builder.build());
- return null;
+ } else {
+ new AsyncTask<Void, Void, Bitmap>() {
+ @Override
+ protected Bitmap doInBackground(Void... arg0) {
+ return BitmapFactory.decodeResource(mActivity.getResources(), imageResId);
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap programPosterArt) {
+ if (mMediaSession != null && programPosterArt != null
+ && isCurrentProgram(program)) {
+ builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt);
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+ }.execute();
}
- }.execute();
+ }
}
// An event was triggered by MediaController.TransportControls and must be handled here.
diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
new file mode 100644
index 00000000..783ae682
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java
@@ -0,0 +1,494 @@
+/*
+ * 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.Fragment;
+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.PlaybackFragment;
+import android.support.v17.leanback.app.PlaybackFragmentGlueHost;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+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.RowPresenter;
+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.dvr.ui.browse.RecordingCardView;
+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 PlaybackFragment {
+ // TODO: Handles audio focus. Deals with block and ratings.
+ private static final String TAG = "DvrPlaybackOverlayFrag";
+ 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<BaseProgram> mRelatedRecordingsRowAdapter;
+ private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter;
+ private DvrDataManager mDvrDataManager;
+ private ContentRatingsManager mContentRatingsManager;
+ private TvView mTvView;
+ private View mBlockScreenView;
+ private ListRow mRelatedRecordingsRow;
+ private int mVerticalPaddingBase;
+ private int mPaddingWithoutRelatedRow;
+ private int mPaddingWithoutSecondaryRow;
+ private int mWindowWidth;
+ private int mWindowHeight;
+ private float mAppliedAspectRatio;
+ private float mWindowAspectRatio;
+ private boolean mPinChecked;
+ private boolean mStarted;
+ 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);
+ mVerticalPaddingBase = getActivity().getResources()
+ .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base);
+ 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();
+ if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
+ mDvrDataManager.addRecordedProgramLoadFinishedListener(
+ new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
+ @Override
+ public void onRecordedProgramLoadFinished() {
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ if (handleIntent(getActivity().getIntent(), true)) {
+ setUpRows();
+ preparePlayback(getActivity().getIntent());
+ }
+ }
+ }
+ );
+ } else if (!handleIntent(getActivity().getIntent(), true)) {
+ 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(PlaybackFragment.BG_LIGHT);
+ setFadingEnabled(true);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mStarted = true;
+ updateVerticalPosition();
+ }
+
+ @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);
+ mRelatedRecordingsRow = getRelatedRecordingsRow();
+ 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);
+ }
+ updateVerticalPosition();
+ mPlaybackControlHelper.getHost().notifyPlaybackRowChanged();
+ }
+ });
+ 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 contentRating) {
+ if (mPinChecked) {
+ mTvView.unblockContent(contentRating);
+ return;
+ }
+ mBlockScreenView.setVisibility(View.VISIBLE);
+ getActivity().getMediaController().getTransportControls().pause();
+ ((DvrPlaybackActivity) getActivity())
+ .setOnPinCheckListener(
+ new PinDialogFragment.OnPinCheckedListener() {
+ @Override
+ public void onPinChecked(
+ boolean checked, int type, String rating) {
+ ((DvrPlaybackActivity) getActivity())
+ .setOnPinCheckListener(null);
+ if (checked) {
+ mPinChecked = true;
+ mTvView.unblockContent(contentRating);
+ mBlockScreenView.setVisibility(View.GONE);
+ getActivity()
+ .getMediaController()
+ .getTransportControls()
+ .play();
+ }
+ }
+ });
+ PinDialogFragment.create(
+ PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR,
+ contentRating.flattenToString())
+ .show(
+ getActivity().getFragmentManager(),
+ PinDialogFragment.DIALOG_TAG);
+ }
+ });
+ setOnItemViewClickedListener(new BaseOnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Object row) {
+ if (itemViewHolder.view instanceof RecordingCardView) {
+ setFadingEnabled(false);
+ long programId = ((RecordedProgram) itemViewHolder.view.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);
+ }
+ }
+ });
+ if (mProgram != null) {
+ setUpRows();
+ 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) {
+ if (mDvrDataManager.isRecordedProgramLoadFinished() && handleIntent(intent, false)) {
+ 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<TvTrackInfo> getTracks(int trackType) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ return mDvrPlayer.getAudioTracks();
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ return mDvrPlayer.getSubtitleTracks();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the ID of the selected track of the given type.
+ */
+ public String getSelectedTrackId(int trackType) {
+ return mDvrPlayer.getSelectedTrackId(trackType);
+ }
+
+ /**
+ * Returns the language setting of the given track type.
+
+ * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE}
+ * or {@link TvTrackInfo#TYPE_AUDIO}.
+ * @return {@code null} if no language has been set for the given track type.
+ */
+ TvTrackInfo getTrackSetting(int trackType) {
+ return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType);
+ }
+
+ /**
+ * Selects the given audio or subtitle track for DVR playback.
+ * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE}
+ * or {@link TvTrackInfo#TYPE_AUDIO}.
+ * @param selectedTrack {@code null} to disable the audio or subtitle track according to
+ * trackType.
+ */
+ void selectTrack(int trackType, TvTrackInfo selectedTrack) {
+ if (mDvrPlayer.isPlaybackPrepared()) {
+ mDvrPlayer.selectTrack(trackType, selectedTrack);
+ }
+ }
+
+ private boolean handleIntent(Intent intent, boolean finishActivity) {
+ mProgram = getProgramFromIntent(intent);
+ if (mProgram == null) {
+ Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found),
+ Toast.LENGTH_SHORT).show();
+ if (finishActivity) {
+ getActivity().finish();
+ }
+ return false;
+ }
+ return true;
+ }
+
+ 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<RecordedProgram> 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);
+ }
+ updateVerticalPosition();
+ mRowsAdapter.notifyArrayItemRangeChanged(1, 1);
+ }
+
+ private void setUpRows() {
+ mPlaybackControlHelper.createControlsRow();
+ mPlaybackControlHelper.setHost(new PlaybackFragmentGlueHost(this));
+ mRowsAdapter = (ArrayObjectAdapter) getAdapter();
+ ClassPresenterSelector selector =
+ (ClassPresenterSelector) mRowsAdapter.getPresenterSelector();
+ selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
+ mRowsAdapter.setPresenterSelector(selector);
+ if (mStarted) {
+ // If it's started before setting up rows, vertical position has not been updated and
+ // should be updated here.
+ updateVerticalPosition();
+ }
+ }
+
+ 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 void updateVerticalPosition() {
+ Boolean hasSecondaryRow = mPlaybackControlHelper.hasSecondaryRow();
+ if (hasSecondaryRow == null) {
+ return;
+ }
+
+ int verticalPadding = mVerticalPaddingBase;
+ if (mRelatedRecordingsRowAdapter.size() == 0) {
+ verticalPadding += mPaddingWithoutRelatedRow;
+ }
+ if (!hasSecondaryRow) {
+ verticalPadding += mPaddingWithoutSecondaryRow;
+ }
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.playback_controls_dock);
+ View view = fragment == null ? null : fragment.getView();
+ if (view != null) {
+ view.setTranslationY(verticalPadding);
+ }
+ }
+
+ private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> {
+ 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<TvTrackInfo> mTrackInfos;
+ private String mSelectedTrackId;
+ private TvTrackInfo mSelectedTrack;
+ private int mTrackType;
+ private DvrPlaybackOverlayFragment mOverlayFragment;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS);
+ mTrackType = mTrackInfos.get(0).getType();
+ mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID);
+ mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager()
+ .findFragmentById(R.id.dvr_playback_controls_fragment));
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState);
+ backgroundView.setBackgroundColor(getResources()
+ .getColor(R.color.lb_playback_controls_background_light));
+ return backgroundView;
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) {
+ actions.add(new GuidedAction.Builder(getActivity())
+ .id(ACTION_ID_NO_SUBTITLE)
+ .title(getString(R.string.closed_caption_option_item_off))
+ .checkSetId(CHECK_SET_ID)
+ .checked(mSelectedTrackId == null)
+ .build());
+ }
+ for (int i = 0; i < mTrackInfos.size(); i++) {
+ TvTrackInfo info = mTrackInfos.get(i);
+ boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId);
+ GuidedAction action = new GuidedAction.Builder(getActivity())
+ .id(i)
+ .title(getTrackLabel(info, i))
+ .checkSetId(CHECK_SET_ID)
+ .checked(checked)
+ .build();
+ actions.add(action);
+ if (checked) {
+ mSelectedTrack = info;
+ }
+ }
+ }
+
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ int actionId = (int) action.getId();
+ mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId));
+ }
+
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ int actionId = (int) action.getId();
+ mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId);
+ TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack);
+ getFragmentManager().popBackStack();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Workaround: when overlay fragment is faded out, any focus will lost due to overlay
+ // fragment's implementation. So we disable overlay fragment's fading here to prevent
+ // losing focus while users are interacting with the side fragment.
+ mOverlayFragment.setFadingEnabled(false);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ // We disable fading of overlay fragment to prevent side fragment from losing focus,
+ // therefore we should resume it here.
+ mOverlayFragment.setFadingEnabled(true);
+ mOverlayFragment.selectTrack(mTrackType, mSelectedTrack);
+ }
+
+ private String getTrackLabel(TvTrackInfo track, int trackIndex) {
+ if (track.getLanguage() != null) {
+ return new Locale(track.getLanguage()).getDisplayName();
+ }
+ return track.getType() == TvTrackInfo.TYPE_SUBTITLE ?
+ getString(R.string.closed_caption_unknown_language, trackIndex + 1)
+ : getString(R.string.multi_audio_unknown_language);
+ }
+
+ @Override
+ protected void onProvideFragmentTransitions() {
+ super.onProvideFragmentTransitions();
+ // Excludes the background scrim from transition to prevent the blinking caused by
+ // hiding the overlay fragment and sliding in the side fragment at the same time.
+ Transition t = getEnterTransition();
+ if (t != null) {
+ t.excludeTarget(R.id.guidedstep_background, true);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
index 5656655c..7226c666 100644
--- a/src/com/android/tv/dvr/DvrPlayer.java
+++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java
@@ -14,20 +14,24 @@
* limitations under the License
*/
-package com.android.tv.dvr;
+package com.android.tv.dvr.ui.playback;
import android.media.PlaybackParams;
+import android.media.session.PlaybackState;
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;
-public class DvrPlayer {
+class DvrPlayer {
private static final String TAG = "DvrPlayer";
private static final boolean DEBUG = false;
@@ -47,12 +51,19 @@ public class DvrPlayer {
private long mInitialSeekPositionMs;
private final TvView mTvView;
private DvrPlayerCallback mCallback;
- private AspectRatioChangedListener mAspectRatioChangedListener;
- private ContentBlockedListener mContentBlockedListener;
+ private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
+ private OnContentBlockedListener mOnContentBlockedListener;
+ private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener;
+ private OnTrackSelectedListener mOnAudioTrackSelectedListener;
+ private OnTrackSelectedListener mOnSubtitleTrackSelectedListener;
+ private String mSelectedAudioTrackId;
+ private String mSelectedSubtitleTrackId;
private float mAspectRatio = Float.NaN;
private int mPlaybackState = PlaybackState.STATE_NONE;
private long mTimeShiftCurrentPositionMs;
private boolean mPauseOnPrepared;
+ private boolean mHasClosedCaption;
+ private boolean mHasMultiAudio;
private final PlaybackParams mPlaybackParams = new PlaybackParams();
private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback();
private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
@@ -75,22 +86,40 @@ public class DvrPlayer {
public void onPlaybackEnded() { }
}
- public interface AspectRatioChangedListener {
+ public interface OnAspectRatioChangedListener {
/**
* Called when the Video's aspect ratio is changed.
+ *
+ * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios.
+ * Listeners should handle it carefully.
*/
void onAspectRatioChanged(float videoAspectRatio);
}
- public interface ContentBlockedListener {
+ public interface OnContentBlockedListener {
/**
* Called when the Video's aspect ratio is changed.
*/
void onContentBlocked(TvContentRating rating);
}
+ public interface OnTracksAvailabilityChangedListener {
+ /**
+ * Called when the Video's subtitle or audio tracks are changed.
+ */
+ void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
+ }
+
+ public interface OnTrackSelectedListener {
+ /**
+ * Called when certain subtitle or audio track is selected.
+ */
+ void onTrackSelected(String selectedTrackId);
+ }
+
public DvrPlayer(TvView tvView) {
mTvView = tvView;
+ mTvView.setCaptionEnabled(true);
mPlaybackParams.setSpeed(1.0f);
setTvViewCallbacks();
setCallback(null);
@@ -236,6 +265,8 @@ public class DvrPlayer {
mTimeShiftCurrentPositionMs = 0;
mPlaybackParams.setSpeed(1.0f);
mProgram = null;
+ mSelectedAudioTrackId = null;
+ mSelectedSubtitleTrackId = null;
}
/**
@@ -250,17 +281,51 @@ public class DvrPlayer {
}
/**
- * Sets listener to aspect ratio changing.
+ * Sets the listener to aspect ratio changing.
+ */
+ public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) {
+ mOnAspectRatioChangedListener = listener;
+ }
+
+ /**
+ * Sets the listener to content blocking.
*/
- public void setAspectRatioChangedListener(AspectRatioChangedListener listener) {
- mAspectRatioChangedListener = listener;
+ public void setOnContentBlockedListener(OnContentBlockedListener listener) {
+ mOnContentBlockedListener = listener;
}
/**
- * Sets listener to content blocking.
+ * Sets the listener to tracks changing.
*/
- public void setContentBlockedListener(ContentBlockedListener listener) {
- mContentBlockedListener = listener;
+ 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;
}
/**
@@ -306,6 +371,32 @@ public class DvrPlayer {
}
/**
+ * Returns the subtitle tracks of the current playback.
+ */
+ public ArrayList<TvTrackInfo> getSubtitleTracks() {
+ return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE));
+ }
+
+ /**
+ * Returns the audio tracks of the current playback.
+ */
+ public ArrayList<TvTrackInfo> getAudioTracks() {
+ return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO));
+ }
+
+ /**
+ * Returns the ID of the selected track of the given type.
+ */
+ public String getSelectedTrackId(int trackType) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ return mSelectedAudioTrackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ return mSelectedSubtitleTrackId;
+ }
+ return null;
+ }
+
+ /**
* Returns if playback of the recorded program is started.
*/
public boolean isPlaybackPrepared() {
@@ -313,6 +404,41 @@ public class DvrPlayer {
&& mPlaybackState != PlaybackState.STATE_CONNECTING;
}
+ /**
+ * Selects the given track.
+ *
+ * @return ID of the selected track.
+ */
+ String selectTrack(int trackType, TvTrackInfo selectedTrack) {
+ String oldSelectedTrackId = getSelectedTrackId(trackType);
+ String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId();
+ if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) {
+ if (selectedTrack == null) {
+ mTvView.selectTrack(trackType, null);
+ return null;
+ } else {
+ List<TvTrackInfo> tracks = mTvView.getTracks(trackType);
+ if (tracks != null && tracks.contains(selectedTrack)) {
+ mTvView.selectTrack(trackType, newSelectedTrackId);
+ return newSelectedTrackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) {
+ // Track not found, disabled closed caption.
+ mTvView.selectTrack(trackType, null);
+ return null;
+ }
+ }
+ }
+ return oldSelectedTrackId;
+ }
+
+ private void setSelectedTrackId(int trackType, String trackId) {
+ if (trackType == TvTrackInfo.TYPE_AUDIO) {
+ mSelectedAudioTrackId = trackId;
+ } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
+ mSelectedSubtitleTrackId = trackId;
+ }
+ }
+
private void setPlaybackSpeed(int speed) {
mPlaybackParams.setSpeed(speed);
mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
@@ -369,28 +495,60 @@ public class DvrPlayer {
if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
&& mPlaybackState == PlaybackState.STATE_CONNECTING) {
mTimeShiftPlayAvailable = true;
+ if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ // onTimeShiftStatusChanged is sometimes called after
+ // onTimeShiftStartPositionChanged is called. In this case,
+ // resumeToWatchedPositionIfNeeded needs to be called here.
+ resumeToWatchedPositionIfNeeded();
+ }
}
}
@Override
- public void onTrackSelected(String inputId, int type, String trackId) {
- if (trackId == null || type != TvTrackInfo.TYPE_VIDEO
- || mAspectRatioChangedListener == null) {
- return;
+ public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
+ boolean hasClosedCaption =
+ !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty();
+ boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1;
+ if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio)
+ && mOnTracksAvailabilityChangedListener != null) {
+ mOnTracksAvailabilityChangedListener
+ .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio);
}
- List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
- if (trackInfos != null) {
- for (TvTrackInfo trackInfo : trackInfos) {
- if (trackInfo.getId().equals(trackId)) {
- float videoAspectRatio = trackInfo.getVideoPixelAspectRatio()
- * trackInfo.getVideoWidth() / trackInfo.getVideoHeight();
- if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
- if (!Float.isNaN(videoAspectRatio)
- && mAspectRatio != videoAspectRatio) {
- mAspectRatioChangedListener
- .onAspectRatioChanged(videoAspectRatio);
- mAspectRatio = videoAspectRatio;
- return;
+ mHasClosedCaption = hasClosedCaption;
+ mHasMultiAudio = hasMultiAudio;
+ }
+
+ @Override
+ public void onTrackSelected(String inputId, int type, String trackId) {
+ if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) {
+ setSelectedTrackId(type, trackId);
+ OnTrackSelectedListener listener = getOnTrackSelectedListener(type);
+ if (listener != null) {
+ listener.onTrackSelected(trackId);
+ }
+ } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null
+ && mOnAspectRatioChangedListener != null) {
+ List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
+ if (trackInfos != null) {
+ for (TvTrackInfo trackInfo : trackInfos) {
+ if (trackInfo.getId().equals(trackId)) {
+ float videoAspectRatio;
+ int videoWidth = trackInfo.getVideoWidth();
+ int videoHeight = trackInfo.getVideoHeight();
+ if (videoWidth > 0 && videoHeight > 0) {
+ videoAspectRatio = trackInfo.getVideoPixelAspectRatio()
+ * trackInfo.getVideoWidth() / trackInfo.getVideoHeight();
+ } else {
+ // Aspect ratio is unknown. Pass the message to listeners.
+ videoAspectRatio = 0;
+ }
+ if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
+ if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) {
+ mOnAspectRatioChangedListener
+ .onAspectRatioChanged(videoAspectRatio);
+ mAspectRatio = videoAspectRatio;
+ return;
+ }
}
}
}
@@ -399,8 +557,8 @@ public class DvrPlayer {
@Override
public void onContentBlocked(String inputId, TvContentRating rating) {
- if (mContentBlockedListener != null) {
- mContentBlockedListener.onContentBlocked(rating);
+ if (mOnContentBlockedListener != null) {
+ mOnContentBlockedListener.onContentBlocked(rating);
}
}
});
diff --git a/src/com/android/tv/experiments/ExperimentFlag.java b/src/com/android/tv/experiments/ExperimentFlag.java
index 8f60c2b5..c0cbd643 100644
--- a/src/com/android/tv/experiments/ExperimentFlag.java
+++ b/src/com/android/tv/experiments/ExperimentFlag.java
@@ -16,12 +16,19 @@
package com.android.tv.experiments;
+import android.support.annotation.VisibleForTesting;
/**
* Experiments return values based on user, device and other criteria.
*/
public final class ExperimentFlag<T> {
- private final T mDefaultValue;
+
+ private static boolean sAllowOverrides = false;
+
+ @VisibleForTesting
+ public static void initForTest() {
+ sAllowOverrides = true;
+ }
/** Returns a boolean experiment */
public static ExperimentFlag<Boolean> createFlag(
@@ -30,6 +37,11 @@ public final class ExperimentFlag<T> {
defaultValue);
}
+ private final T mDefaultValue;
+
+ private T mOverrideValue = null;
+ private boolean mOverridden = false;
+
private ExperimentFlag(
T defaultValue) {
mDefaultValue = defaultValue;
@@ -37,6 +49,22 @@ public final class ExperimentFlag<T> {
/** Returns value for this experiment */
public T get() {
- return mDefaultValue;
+ return sAllowOverrides && mOverridden ? mOverrideValue : mDefaultValue;
}
+
+ @VisibleForTesting
+ public void override(T t) {
+ if (sAllowOverrides) {
+ mOverridden = true;
+ mOverrideValue = t;
+ }
+ }
+
+ @VisibleForTesting
+ public void resetOverride() {
+ mOverridden = false;
+ }
+
+
+
}
diff --git a/src/com/android/tv/experiments/Experiments.java b/src/com/android/tv/experiments/Experiments.java
index f16c8d1e..53cce979 100644
--- a/src/com/android/tv/experiments/Experiments.java
+++ b/src/com/android/tv/experiments/Experiments.java
@@ -23,12 +23,15 @@ import com.android.tv.common.BuildConfig;
/**
* Set of experiments visible in AOSP.
*
- * <p>
- * This file is maintained by hand.
+ * <p>This file is maintained by hand.
*/
public final class Experiments {
public static final ExperimentFlag<Boolean> CLOUD_EPG = createFlag(
- false);
+ true);
+
+ public static final ExperimentFlag<Boolean> ENABLE_UNRATED_CONTENT_SETTINGS =
+ createFlag(
+ false);
/**
* Allow developer features such as the dev menu and other aids.
diff --git a/src/com/android/tv/guide/GenreListAdapter.java b/src/com/android/tv/guide/GenreListAdapter.java
index 2913599c..ce19eb2d 100644
--- a/src/com/android/tv/guide/GenreListAdapter.java
+++ b/src/com/android/tv/guide/GenreListAdapter.java
@@ -17,6 +17,7 @@
package com.android.tv.guide;
import android.content.Context;
+import android.support.annotation.MainThread;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -32,7 +33,7 @@ import java.util.List;
/**
* Adapts the genre items obtained from {@link GenreItems} to the program guide side panel.
*/
-public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHolder> {
+class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHolder> {
private static final String TAG = "GenreListAdapter";
private static final boolean DEBUG = false;
@@ -41,7 +42,7 @@ public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.Genr
private final ProgramGuide mProgramGuide;
private String[] mGenreLabels;
- public GenreListAdapter(Context context, ProgramManager programManager, ProgramGuide guide) {
+ GenreListAdapter(Context context, ProgramManager programManager, ProgramGuide guide) {
mContext = context;
mProgramManager = programManager;
mProgramManager.addListener(new ProgramManager.ListenerAdapter() {
@@ -79,16 +80,28 @@ public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.Genr
@Override
public GenreRowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
+ itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ // Animation is not meaningful now, skip it.
+ view.getStateListAnimator().jumpToCurrentState();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ // Do nothing
+ }
+ });
return new GenreRowHolder(itemView, mProgramGuide);
}
- public static class GenreRowHolder extends RecyclerView.ViewHolder implements
+ static class GenreRowHolder extends RecyclerView.ViewHolder implements
View.OnFocusChangeListener {
private final ProgramGuide mProgramGuide;
private int mGenreId;
- // Should be called from main thread.
- public GenreRowHolder(View itemView, ProgramGuide programGuide) {
+ @MainThread
+ GenreRowHolder(View itemView, ProgramGuide programGuide) {
super(itemView);
mProgramGuide = programGuide;
}
diff --git a/src/com/android/tv/guide/GuideUtils.java b/src/com/android/tv/guide/GuideUtils.java
index 5d11f061..403d00b5 100644
--- a/src/com/android/tv/guide/GuideUtils.java
+++ b/src/com/android/tv/guide/GuideUtils.java
@@ -16,30 +16,38 @@
package com.android.tv.guide;
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
-public class GuideUtils {
+class GuideUtils {
+ private static final int INVALID_INDEX = -1;
private static int sWidthPerHour = 0;
/**
* Sets the width in pixels that corresponds to an hour in program guide.
* Assume that this is called from main thread only, so, no synchronization.
*/
- public static void setWidthPerHour(int widthPerHour) {
+ static void setWidthPerHour(int widthPerHour) {
sWidthPerHour = widthPerHour;
}
/**
* Gets the number of pixels in program guide table that corresponds to the given milliseconds.
*/
- public static int convertMillisToPixel(long millis) {
+ static int convertMillisToPixel(long millis) {
return (int) (millis * sWidthPerHour / TimeUnit.HOURS.toMillis(1));
}
/**
* Gets the number of pixels in program guide table that corresponds to the given range.
*/
- public static int convertMillisToPixel(long startMillis, long endMillis) {
+ static int convertMillisToPixel(long startMillis, long endMillis) {
// Convert to pixels first to avoid accumulation of rounding errors.
return GuideUtils.convertMillisToPixel(endMillis)
- GuideUtils.convertMillisToPixel(startMillis);
@@ -48,9 +56,101 @@ public class GuideUtils {
/**
* Gets the time in millis that corresponds to the given pixels in the program guide.
*/
- public static long convertPixelToMillis(int pixel) {
+ static long convertPixelToMillis(int pixel) {
return pixel * TimeUnit.HOURS.toMillis(1) / sWidthPerHour;
}
+ /**
+ * Return the view should be focused in the given program row according to the focus range.
+
+ * @param keepCurrentProgramFocused If {@code true}, focuses on the current program if possible,
+ * else falls back the general logic.
+ */
+ static View findNextFocusedProgram(View programRow, int focusRangeLeft,
+ int focusRangeRight, boolean keepCurrentProgramFocused) {
+ ArrayList<View> focusables = new ArrayList<>();
+ findFocusables(programRow, focusables);
+
+ if (keepCurrentProgramFocused) {
+ // Select the current program if possible.
+ for (int i = 0; i < focusables.size(); ++i) {
+ View focusable = focusables.get(i);
+ if (focusable instanceof ProgramItemView
+ && isCurrentProgram((ProgramItemView) focusable)) {
+ return focusable;
+ }
+ }
+ }
+
+ // Find the largest focusable among fully overlapped focusables.
+ int maxFullyOverlappedWidth = Integer.MIN_VALUE;
+ int maxPartiallyOverlappedWidth = Integer.MIN_VALUE;
+ int nextFocusIndex = INVALID_INDEX;
+ for (int i = 0; i < focusables.size(); ++i) {
+ View focusable = focusables.get(i);
+ Rect focusableRect = new Rect();
+ focusable.getGlobalVisibleRect(focusableRect);
+ if (focusableRect.left <= focusRangeLeft && focusRangeRight <= focusableRect.right) {
+ // the old focused range is fully inside the focusable, return directly.
+ return focusable;
+ } else if (focusRangeLeft <= focusableRect.left
+ && focusableRect.right <= focusRangeRight) {
+ // the focusable is fully inside the old focused range, choose the widest one.
+ int width = focusableRect.width();
+ if (width > maxFullyOverlappedWidth) {
+ nextFocusIndex = i;
+ maxFullyOverlappedWidth = width;
+ }
+ } else if (maxFullyOverlappedWidth == Integer.MIN_VALUE) {
+ int overlappedWidth = (focusRangeLeft <= focusableRect.left) ?
+ focusRangeRight - focusableRect.left
+ : focusableRect.right - focusRangeLeft;
+ if (overlappedWidth > maxPartiallyOverlappedWidth) {
+ nextFocusIndex = i;
+ maxPartiallyOverlappedWidth = overlappedWidth;
+ }
+ }
+ }
+ if (nextFocusIndex != INVALID_INDEX) {
+ return focusables.get(nextFocusIndex);
+ }
+ return null;
+ }
+
+ /**
+ * Returns {@code true} if the program displayed in the give
+ * {@link com.android.tv.guide.ProgramItemView} is a current program.
+ */
+ static boolean isCurrentProgram(ProgramItemView view) {
+ return view.getTableEntry().isCurrentProgram();
+ }
+
+ /**
+ * Returns {@code true} if the given view is a descendant of the give container.
+ */
+ static boolean isDescendant(ViewGroup container, View view) {
+ if (view == null) {
+ return false;
+ }
+ for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
+ if (p == container) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void findFocusables(View v, ArrayList<View> outFocusable) {
+ if (v.isFocusable()) {
+ outFocusable.add(v);
+ }
+ if (v instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) v;
+ for (int i = 0; i < viewGroup.getChildCount(); ++i) {
+ findFocusables(viewGroup.getChildAt(i), outFocusable);
+ }
+ }
+ }
+
private GuideUtils() { }
}
diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java
index 77de5827..58436425 100644
--- a/src/com/android/tv/guide/ProgramGrid.java
+++ b/src/com/android/tv/guide/ProgramGrid.java
@@ -20,17 +20,15 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView.LayoutManager;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Range;
import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import com.android.tv.R;
import com.android.tv.ui.OnRepeatedKeyInterceptListener;
-import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
@@ -52,7 +50,7 @@ public class ProgramGrid extends VerticalGridView {
clearUpDownFocusState(newFocus);
}
mNextFocusByUpDown = null;
- if (newFocus != ProgramGrid.this && contains(newFocus)) {
+ if (GuideUtils.isDescendant(ProgramGrid.this, newFocus)) {
mLastFocusedView = newFocus;
}
}
@@ -90,8 +88,9 @@ public class ProgramGrid extends VerticalGridView {
private View mLastFocusedView;
private final Rect mTempRect = new Rect();
+ private int mLastUpDownDirection;
- private boolean mKeepCurrentProgram;
+ private boolean mKeepCurrentProgramFocused;
private ChildFocusListener mChildFocusListener;
private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener;
@@ -132,21 +131,6 @@ public class ProgramGrid extends VerticalGridView {
setOnKeyInterceptListener(mOnRepeatedKeyInterceptListener);
}
- /**
- * Initializes ProgramGrid. It should be called before the view is actually attached to
- * Window.
- */
- public void initialize(ProgramManager programManager) {
- mProgramManager = programManager;
- }
-
- /**
- * Registers a listener focus events occurring on children to the {@code ProgramGrid}.
- */
- public void setChildFocusListener(ChildFocusListener childFocusListener) {
- mChildFocusListener = childFocusListener;
- }
-
@Override
public void requestChildFocus(View child, View focused) {
if (mChildFocusListener != null) {
@@ -173,11 +157,11 @@ public class ProgramGrid extends VerticalGridView {
@Override
public View focusSearch(View focused, int direction) {
mNextFocusByUpDown = null;
- if (focused == null || !contains(focused)) {
+ if (focused == null || (focused != this && !GuideUtils.isDescendant(this, focused))) {
return super.focusSearch(focused, direction);
}
if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
- updateUpDownFocusState(focused);
+ updateUpDownFocusState(focused, direction);
View nextFocus = focusFind(focused, direction);
if (nextFocus != null) {
return nextFocus;
@@ -186,15 +170,85 @@ public class ProgramGrid extends VerticalGridView {
return super.focusSearch(focused, direction);
}
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ if (mLastFocusedView != null && mLastFocusedView.isShown()) {
+ if (mLastFocusedView.requestFocus()) {
+ return true;
+ }
+ }
+ return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+
+ @Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused
+ // item's are at the almost end of screen, focus change to the next item doesn't work.
+ // It restricts that a focus item's position cannot be too far from the desired position.
+ View focusedView = findFocus();
+ if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) {
+ int[] location = new int[2];
+ getLocationOnScreen(location);
+ int[] focusedLocation = new int[2];
+ focusedView.getLocationOnScreen(focusedLocation);
+ int y = focusedLocation[1] - location[1];
+ int minY = (mSelectionRow - 1) * mRowHeight;
+ if (y < minY) scrollBy(0, y - minY);
+ int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight;
+ if (y > maxY) scrollBy(0, y - maxY);
+ }
+ updateInputLogo();
+ }
+
+ @Override
+ public void onViewRemoved(View view) {
+ // It is required to ensure input logo showing when the scroll is moved to most bottom.
+ updateInputLogo();
+ }
+
+ /**
+ * Initializes ProgramGrid. It should be called before the view is actually attached to
+ * Window.
+ */
+ void initialize(ProgramManager programManager) {
+ mProgramManager = programManager;
+ }
+
+ /**
+ * Registers a listener focus events occurring on children to the {@code ProgramGrid}.
+ */
+ void setChildFocusListener(ChildFocusListener childFocusListener) {
+ mChildFocusListener = childFocusListener;
+ }
+
+ void onItemSelectionReset() {
+ getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
+ }
+
/**
* Resets focus states. If the logic to keep the last focus needs to be cleared, it should
* be called.
*/
- public void resetFocusState() {
+ void resetFocusState() {
mLastFocusedView = null;
clearUpDownFocusState(null);
}
+ /** Returns the currently focused item's horizontal range. */
+ Range<Integer> getFocusRange() {
+ return new Range<>(mFocusRangeLeft, mFocusRangeRight);
+ }
+
+ /** Returns if the next focused item should be the current program if possible. */
+ boolean isKeepCurrentProgramFocused() {
+ return mKeepCurrentProgramFocused;
+ }
+
+ /** Returns the last up/down move direction of browsing */
+ int getLastUpDownDirection() {
+ return mLastUpDownDirection;
+ }
+
private View focusFind(View focused, int direction) {
int focusedChildIndex = getFocusedChildIndex();
if (focusedChildIndex == INVALID_INDEX) {
@@ -204,85 +258,26 @@ public class ProgramGrid extends VerticalGridView {
int nextChildIndex = direction == View.FOCUS_UP ? focusedChildIndex - 1
: focusedChildIndex + 1;
if (nextChildIndex < 0 || nextChildIndex >= getChildCount()) {
- return focused;
- }
- View nextChild = getChildAt(nextChildIndex);
- ArrayList<View> focusables = new ArrayList<>();
- findFocusables(nextChild, focusables);
-
- int index = INVALID_INDEX;
- if (mKeepCurrentProgram) {
- // Select the current program if possible.
- for (int i = 0; i < focusables.size(); ++i) {
- View focusable = focusables.get(i);
- if (!(focusable instanceof ProgramItemView)) {
- continue;
- }
- if (((ProgramItemView) focusable).getTableEntry().isCurrentProgram()) {
- index = i;
- break;
- }
- }
- if (index != INVALID_INDEX) {
- mNextFocusByUpDown = focusables.get(index);
- return mNextFocusByUpDown;
- } else {
- mKeepCurrentProgram = false;
- }
- }
-
- // Find the largest focusable among fully overlapped focusables.
- int maxWidth = Integer.MIN_VALUE;
- for (int i = 0; i < focusables.size(); ++i) {
- View focusable = focusables.get(i);
- Rect focusableRect = mTempRect;
- focusable.getGlobalVisibleRect(focusableRect);
- if (mFocusRangeLeft <= focusableRect.left && focusableRect.right <= mFocusRangeRight) {
- int width = focusableRect.width();
- if (width > maxWidth) {
- index = i;
- maxWidth = width;
- }
- } else if (focusableRect.left <= mFocusRangeLeft
- && mFocusRangeRight <= focusableRect.right) {
- // focusableRect contains [mLeft, mRight].
- index = i;
- break;
- }
- }
- if (index != INVALID_INDEX) {
- mNextFocusByUpDown = focusables.get(index);
- return mNextFocusByUpDown;
- }
-
- // Find the largest overlapped view among partially overlapped focusables.
- maxWidth = Integer.MIN_VALUE;
- for (int i = 0; i < focusables.size(); ++i) {
- View focusable = focusables.get(i);
- Rect focusableRect = mTempRect;
- focusable.getGlobalVisibleRect(focusableRect);
- if (mFocusRangeLeft <= focusableRect.left && focusableRect.left <= mFocusRangeRight) {
- int overlappedWidth = mFocusRangeRight - focusableRect.left;
- if (overlappedWidth > maxWidth) {
- index = i;
- maxWidth = overlappedWidth;
- }
- } else if (mFocusRangeLeft <= focusableRect.right
- && focusableRect.right <= mFocusRangeRight) {
- int overlappedWidth = focusableRect.right - mFocusRangeLeft;
- if (overlappedWidth > maxWidth) {
- index = i;
- maxWidth = overlappedWidth;
- }
+ // Wraparound if reached head or end
+ if (getSelectedPosition() == 0) {
+ scrollToPosition(getAdapter().getItemCount() - 1);
+ return null;
+ } else if (getSelectedPosition() == getAdapter().getItemCount() - 1) {
+ scrollToPosition(0);
+ return null;
}
+ return focused;
}
- if (index != INVALID_INDEX) {
- mNextFocusByUpDown = focusables.get(index);
- return mNextFocusByUpDown;
+ View nextFocusedProgram = GuideUtils.findNextFocusedProgram(getChildAt(nextChildIndex),
+ mFocusRangeLeft, mFocusRangeRight, mKeepCurrentProgramFocused);
+ if (nextFocusedProgram != null) {
+ nextFocusedProgram.getGlobalVisibleRect(mTempRect);
+ mNextFocusByUpDown = nextFocusedProgram;
+
+ } else {
+ Log.w(TAG, "focusFind doesn't find proper focusable");
}
-
- Log.w(TAG, "focusFind doesn't find proper focusable");
- return null;
+ return nextFocusedProgram;
}
// Returned value is not the position of VerticalGridView. But it's the index of ViewGroup
@@ -296,7 +291,8 @@ public class ProgramGrid extends VerticalGridView {
return INVALID_INDEX;
}
- private void updateUpDownFocusState(View focused) {
+ private void updateUpDownFocusState(View focused, int direction) {
+ mLastUpDownDirection = direction;
int rightMostFocusablePosition = getRightMostFocusablePosition();
Rect focusedRect = mTempRect;
@@ -319,11 +315,13 @@ public class ProgramGrid extends VerticalGridView {
}
private void clearUpDownFocusState(View focus) {
+ mLastUpDownDirection = 0;
mFocusRangeLeft = 0;
mFocusRangeRight = getRightMostFocusablePosition();
mNextFocusByUpDown = null;
- mKeepCurrentProgram = focus != null && focus instanceof ProgramItemView
- && ((ProgramItemView) focus).getTableEntry().isCurrentProgram();
+ // If focus is not a program item, drop focus to the current program when back to the grid
+ mKeepCurrentProgramFocused = !(focus instanceof ProgramItemView)
+ || GuideUtils.isCurrentProgram((ProgramItemView) focus);
}
private int getRightMostFocusablePosition() {
@@ -333,56 +331,6 @@ public class ProgramGrid extends VerticalGridView {
return mTempRect.right - GuideUtils.convertMillisToPixel(FOCUS_AREA_RIGHT_MARGIN_MILLIS);
}
- private boolean contains(View v) {
- if (v == this) {
- return true;
- }
- if (v == null || v == v.getRootView()) {
- return false;
- }
- return contains((View) v.getParent());
- }
-
- public void onItemSelectionReset() {
- getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
- }
-
- @Override
- public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- if (mLastFocusedView != null && mLastFocusedView.isShown()) {
- if (mLastFocusedView.requestFocus()) {
- return true;
- }
- }
- return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
- }
-
- @Override
- protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused
- // item's are at the almost end of screen, focus change to the next item doesn't work.
- // It restricts that a focus item's position cannot be too far from the desired position.
- View focusedView = findFocus();
- if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) {
- int[] location = new int[2];
- getLocationOnScreen(location);
- int[] focusedLocation = new int[2];
- focusedView.getLocationOnScreen(focusedLocation);
- int y = focusedLocation[1] - location[1];
- int minY = (mSelectionRow - 1) * mRowHeight;
- if (y < minY) scrollBy(0, y - minY);
- int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight;
- if (y > maxY) scrollBy(0, y - maxY);
- }
- updateInputLogo();
- }
-
- @Override
- public void onViewRemoved(View view) {
- // It is required to ensure input logo showing when the scroll is moved to most bottom.
- updateInputLogo();
- }
-
private int getFirstVisibleChildIndex() {
final LayoutManager mLayoutManager = getLayoutManager();
int top = mLayoutManager.getPaddingTop();
@@ -398,7 +346,7 @@ public class ProgramGrid extends VerticalGridView {
return -1;
}
- public void updateInputLogo() {
+ private void updateInputLogo() {
int childCount = getChildCount();
if (childCount == 0) {
return;
@@ -409,25 +357,13 @@ public class ProgramGrid extends VerticalGridView {
}
View childView = getChildAt(firstVisibleChildIndex);
int childAdapterPosition = getChildAdapterPosition(childView);
- ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView))
+ ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
.updateInputLogo(childAdapterPosition, true);
for (int i = firstVisibleChildIndex + 1; i < childCount; i++) {
childView = getChildAt(i);
- ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView))
+ ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
.updateInputLogo(childAdapterPosition, false);
childAdapterPosition = getChildAdapterPosition(childView);
}
}
-
- private static void findFocusables(View v, ArrayList<View> outFocusable) {
- if (v.isFocusable()) {
- outFocusable.add(v);
- }
- if (v instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) v;
- for (int i = 0; i < viewGroup.getChildCount(); ++i) {
- findFocusables(viewGroup.getChildAt(i), outFocusable);
- }
- }
- }
}
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 120b3dba..dd5444e2 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;
@@ -143,6 +143,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS;
private boolean mIsDuringResetRowSelection;
private final Handler mHandler = new ProgramGuideHandler(this);
+ private boolean mActive;
private final Runnable mHideRunnable = new Runnable() {
@Override
@@ -217,7 +218,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
.getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y));
mSidePanelGridView.setWindowAlignmentOffsetPercent(
VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- // TODO: Remove this check when we ship TV with epg search enabled.
+
if (Features.EPG_SEARCH.isEnabled(mActivity)) {
mSearchOrb = (SearchOrbView) mContainer.findViewById(
R.id.program_guide_side_panel_search_orb);
@@ -250,8 +251,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item));
mTimelineRow.setAdapter(mTimeListAdapter);
- ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity,
- mProgramManager, this);
+ ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, this);
programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
@@ -304,13 +304,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
R.animator.program_guide_side_panel_enter_full,
0,
R.animator.program_guide_table_enter_full);
- mShowAnimatorFull.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- ((ViewGroup) mSidePanel).setDescendantFocusability(
- ViewGroup.FOCUS_AFTER_DESCENDANTS);
- }
- });
mShowAnimatorPartial = createAnimator(
R.animator.program_guide_side_panel_enter_partial,
@@ -383,34 +376,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
|| mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
}
- private void updateGuidePosition() {
- // Align EPG at vertical center, if EPG table height is less than the screen size.
- Resources res = mActivity.getResources();
- int screenHeight = mContainer.getHeight();
- if (screenHeight <= 0) {
- // mContainer is not initialized yet.
- return;
- }
- int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
- int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
- int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
- int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
- + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding
- + bottomPadding;
- if (tableHeight > screenHeight) {
- // EPG height is longer that the screen height.
- mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
- LayoutParams layoutParams = mTable.getLayoutParams();
- layoutParams.height = LayoutParams.WRAP_CONTENT;
- mTable.setLayoutParams(layoutParams);
- } else {
- mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
- LayoutParams layoutParams = mTable.getLayoutParams();
- layoutParams.height = tableHeight;
- mTable.setLayoutParams(layoutParams);
- }
- }
-
@Override
public void onRequestChildFocus(View oldFocus, View newFocus) {
if (oldFocus != null && newFocus != null) {
@@ -431,40 +396,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
}
- private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId,
- int tableAnimResId) {
- List<Animator> animatorList = new ArrayList<>();
-
- Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId);
- sidePanelAnimator.setTarget(mSidePanel);
- animatorList.add(sidePanelAnimator);
-
- if (sidePanelGridAnimResId != 0) {
- Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity,
- sidePanelGridAnimResId);
- sidePanelGridAnimator.setTarget(mSidePanelGridView);
- sidePanelGridAnimator.addListener(
- new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView));
- animatorList.add(sidePanelGridAnimator);
- }
- Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId);
- tableAnimator.setTarget(mTable);
- tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
- animatorList.add(tableAnimator);
-
- AnimatorSet set = new AnimatorSet();
- set.playTogether(animatorList);
- return set;
- }
-
- /**
- * Returns {@code true} if the program guide should process the input events.
- */
- public boolean isActive() {
- return mContainer.getVisibility() == View.VISIBLE && !mHideAnimatorFull.isStarted()
- && !mHideAnimatorPartial.isStarted();
- }
-
/**
* Show the program guide. This reveals the side panel, and the program guide table is shown
* partially.
@@ -494,14 +425,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mTimeListAdapter.update(mStartUtcTime);
mTimelineRow.resetScroll();
+ mContainer.setVisibility(View.VISIBLE);
+ mActive = true;
if (!mShowGuidePartial) {
- // Avoid changing focus from the genre side panel to the grid during animation.
- // The descendant focus is changed to FOCUS_AFTER_DESCENDANTS after the animation.
- ((ViewGroup) mSidePanel).setDescendantFocusability(
- ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ mTable.requestFocus();
}
-
- mContainer.setVisibility(View.VISIBLE);
positionCurrentTimeIndicator();
mSidePanelGridView.setSelectedPosition(0);
if (DEBUG) {
@@ -536,13 +464,13 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
});
}
+ updateGuidePosition();
runnableAfterAnimatorReady.run();
if (mShowGuidePartial) {
mShowAnimatorPartial.start();
} else {
mShowAnimatorFull.start();
}
- updateGuidePosition();
}
};
mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow);
@@ -564,7 +492,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
cancelHide();
mProgramManager.programGuideVisibilityChanged(false);
mProgramManager.removeListener(mProgramManagerListener);
- if (isFull()) {
+ mActive = false;
+ if (!mShowGuidePartial) {
mHideAnimatorFull.start();
} else {
mHideAnimatorPartial.start();
@@ -587,50 +516,21 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
}
+ /**
+ * Schedules hiding the program guide.
+ */
public void scheduleHide() {
cancelHide();
mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
}
/**
- * Returns the scroll offset of the time line row in pixels.
- */
- public int getTimelineRowScrollOffset() {
- return mTimelineRow.getScrollOffset();
- }
-
- /**
- * Cancel hiding the program guide.
+ * Cancels hiding the program guide.
*/
public void cancelHide() {
mHandler.removeCallbacks(mHideRunnable);
}
- // Returns if program table is full screen mode.
- private boolean isFull() {
- return mPartialToFullAnimator.isStarted() || mTable.getTranslationX() == 0;
- }
-
- private void startFull() {
- if (isFull() || mAccessibilityManager.isEnabled()) {
- // If accessibility service is enabled, focus cannot be moved to side panel due to it's
- // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
- return;
- }
- mShowGuidePartial = false;
- mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
- mPartialToFullAnimator.start();
- }
-
- private void startPartial() {
- if (!isFull()) {
- return;
- }
- mShowGuidePartial = true;
- mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
- mFullToPartialAnimator.start();
- }
-
/**
* Process the {@code KEYCODE_BACK} key event.
*/
@@ -639,16 +539,30 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
/**
- * Gets {@link VerticalGridView} for "genre select" side panel.
+ * Returns {@code true} if the program guide should process the input events.
*/
- public VerticalGridView getSidePanel() {
- return mSidePanelGridView;
+ public boolean isActive() {
+ return mActive;
+ }
+
+ /**
+ * Returns {@code true} if the program guide is shown, i.e. showing animation is done and
+ * hiding animation is not started yet.
+ */
+ public boolean isRunningAnimation() {
+ return mShowAnimatorPartial.isStarted() || mShowAnimatorFull.isStarted()
+ || mHideAnimatorPartial.isStarted() || mHideAnimatorFull.isStarted();
+ }
+
+ /** Returns if program table is in full screen mode. **/
+ boolean isFull() {
+ return !mShowGuidePartial;
}
/**
* Requests change genre to {@code genreId}.
*/
- public void requestGenreChange(int genreId) {
+ void requestGenreChange(int genreId) {
if (mLastRequestedGenreId == genreId) {
// When Recycler.onLayout() removes its children to recycle,
// View tries to find next focus candidate immediately
@@ -679,6 +593,104 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mProgramTableFadeOutAnimator.start();
}
+ /**
+ * Returns the scroll offset of the time line row in pixels.
+ */
+ int getTimelineRowScrollOffset() {
+ return mTimelineRow.getScrollOffset();
+ }
+
+ /** Returns the program grid view that hold all component views. */
+ ProgramGrid getProgramGrid() {
+ return mGrid;
+ }
+
+ /**
+ * Gets {@link VerticalGridView} for "genre select" side panel.
+ */
+ VerticalGridView getSidePanel() {
+ return mSidePanelGridView;
+ }
+
+ /** Returns the program manager the program guide is using to provide program information. */
+ ProgramManager getProgramManager() {
+ return mProgramManager;
+ }
+
+ private void updateGuidePosition() {
+ // Align EPG at vertical center, if EPG table height is less than the screen size.
+ Resources res = mActivity.getResources();
+ int screenHeight = mContainer.getHeight();
+ if (screenHeight <= 0) {
+ // mContainer is not initialized yet.
+ return;
+ }
+ int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
+ int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
+ int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
+ int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
+ + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding
+ + bottomPadding;
+ if (tableHeight > screenHeight) {
+ // EPG height is longer that the screen height.
+ mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = LayoutParams.WRAP_CONTENT;
+ mTable.setLayoutParams(layoutParams);
+ } else {
+ mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = tableHeight;
+ mTable.setLayoutParams(layoutParams);
+ }
+ }
+
+ private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId,
+ int tableAnimResId) {
+ List<Animator> animatorList = new ArrayList<>();
+
+ Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId);
+ sidePanelAnimator.setTarget(mSidePanel);
+ animatorList.add(sidePanelAnimator);
+
+ if (sidePanelGridAnimResId != 0) {
+ Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity,
+ sidePanelGridAnimResId);
+ sidePanelGridAnimator.setTarget(mSidePanelGridView);
+ sidePanelGridAnimator.addListener(
+ new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView));
+ animatorList.add(sidePanelGridAnimator);
+ }
+ Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId);
+ tableAnimator.setTarget(mTable);
+ tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
+ animatorList.add(tableAnimator);
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animatorList);
+ return set;
+ }
+
+ private void startFull() {
+ if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) {
+ // If accessibility service is enabled, focus cannot be moved to side panel due to it's
+ // hidden. Therefore, we don't hide side panel when accessibility service is enabled.
+ return;
+ }
+ mShowGuidePartial = false;
+ mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
+ mPartialToFullAnimator.start();
+ }
+
+ private void startPartial() {
+ if (mShowGuidePartial) {
+ return;
+ }
+ mShowGuidePartial = true;
+ mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
+ mFullToPartialAnimator.start();
+ }
+
private void startCurrentTimeIndicator(long initialDelay) {
mHandler.postDelayed(mUpdateTimeIndicator, initialDelay);
}
@@ -775,10 +787,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mDetailInAnimator.cancel();
}
- int direction = 0;
- if (outRow != null && inRow != null) {
- // -1 means the selection goes downwards and 1 goes upwards
- direction = outRow.getTop() < inRow.getTop() ? -1 : 1;
+ int operationDirection = mGrid.getLastUpDownDirection();
+ int animationPadding = 0;
+ if (operationDirection == View.FOCUS_UP) {
+ animationPadding = mDetailPadding;
+ } else if (operationDirection == View.FOCUS_DOWN) {
+ animationPadding = -mDetailPadding;
}
View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null;
@@ -788,7 +802,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
Animator fadeOutAnimator = ObjectAnimator.ofPropertyValuesHolder(outDetailContent,
PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
- outDetailContent.getTranslationY(), direction * mDetailPadding));
+ outDetailContent.getTranslationY(), animationPadding));
fadeOutAnimator.setStartDelay(0);
fadeOutAnimator.setDuration(mAnimationDuration);
fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent));
@@ -842,8 +856,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
});
Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent,
PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
- PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
- direction * -mDetailPadding, 0f));
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -animationPadding, 0f));
fadeInAnimator.setDuration(mAnimationDuration);
fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent));
@@ -910,7 +923,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> {
- public ProgramGuideHandler(ProgramGuide ref) {
+ ProgramGuideHandler(ProgramGuide ref) {
super(ref);
}
diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java
index 4c7a4404..b23d578c 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;
@@ -73,6 +73,7 @@ public class ProgramItemView extends TextView {
private static TextAppearanceSpan sEpisodeTitleStyle;
private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle;
+ private ProgramGuide mProgramGuide;
private DvrManager mDvrManager;
private TableEntry mTableEntry;
private int mMaxWidthForRipple;
@@ -106,18 +107,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(
@@ -158,6 +160,11 @@ public class ProgramItemView extends TextView {
}
if (entry.isCurrentProgram()) {
Drawable background = getBackground();
+ if (!mProgramGuide.isActive() || mProgramGuide.isRunningAnimation()) {
+ // If program guide is not active or is during showing/hiding,
+ // the animation is unnecessary, skip it.
+ background.jumpToCurrentState();
+ }
int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis);
setProgress(background, R.id.reverse_progress, MAX_PROGRESS - progress);
}
@@ -247,8 +254,9 @@ public class ProgramItemView extends TextView {
}
@SuppressLint("SwitchIntDef")
- public void setValues(TableEntry entry, int selectedGenreId, long fromUtcMillis,
- long toUtcMillis, String gapTitle) {
+ public void setValues(ProgramGuide programGuide, TableEntry entry, int selectedGenreId,
+ long fromUtcMillis, long toUtcMillis, String gapTitle) {
+ mProgramGuide = programGuide;
mTableEntry = entry;
ViewGroup.LayoutParams layoutParams = getLayoutParams();
@@ -376,6 +384,7 @@ public class ProgramItemView extends TextView {
}
setTag(null);
+ mProgramGuide = null;
mTableEntry = null;
}
diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java
index 03aea5ad..c1fcdd40 100644
--- a/src/com/android/tv/guide/ProgramListAdapter.java
+++ b/src/com/android/tv/guide/ProgramListAdapter.java
@@ -32,11 +32,12 @@ import com.android.tv.guide.ProgramManager.TableEntry;
* Adapts a program list for a specific channel from {@link ProgramManager} to a row of the program
* guide table.
*/
-public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder>
+class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramItemViewHolder>
implements TableEntriesUpdatedListener {
private static final String TAG = "ProgramListAdapter";
private static final boolean DEBUG = false;
+ private final ProgramGuide mProgramGuide;
private final ProgramManager mProgramManager;
private final int mChannelIndex;
private final String mNoInfoProgramTitle;
@@ -44,9 +45,10 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.
private long mChannelId;
- public ProgramListAdapter(Resources res, ProgramManager programManager, int channelIndex) {
+ ProgramListAdapter(Resources res, ProgramGuide programGuide, int channelIndex) {
setHasStableIds(true);
- mProgramManager = programManager;
+ mProgramGuide = programGuide;
+ mProgramManager = programGuide.getProgramManager();
mChannelIndex = channelIndex;
mNoInfoProgramTitle = res.getString(R.string.program_title_for_no_information);
mBlockedProgramTitle = res.getString(R.string.program_title_for_blocked_channel);
@@ -65,10 +67,6 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.
}
}
- public ProgramManager getProgramManager() {
- return mProgramManager;
- }
-
@Override
public int getItemCount() {
return mProgramManager.getTableEntryCount(mChannelId);
@@ -85,38 +83,40 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.
}
@Override
- public void onBindViewHolder(ProgramViewHolder holder, int position) {
+ public void onBindViewHolder(ProgramItemViewHolder holder, int position) {
TableEntry tableEntry = mProgramManager.getTableEntry(mChannelId, position);
String gapTitle = tableEntry.isBlocked() ? mBlockedProgramTitle : mNoInfoProgramTitle;
- holder.onBind(tableEntry, this.getProgramManager(), gapTitle);
+ holder.onBind(tableEntry, mProgramGuide, gapTitle);
}
@Override
- public void onViewRecycled(ProgramViewHolder holder) {
+ public void onViewRecycled(ProgramItemViewHolder holder) {
holder.onUnbind();
}
@Override
- public ProgramViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public ProgramItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
- return new ProgramViewHolder(itemView);
+ return new ProgramItemViewHolder(itemView);
}
- public static class ProgramViewHolder extends RecyclerView.ViewHolder {
+ static class ProgramItemViewHolder extends RecyclerView.ViewHolder {
// Should be called from main thread.
- public ProgramViewHolder(View itemView) {
+ ProgramItemViewHolder(View itemView) {
super(itemView);
}
- public void onBind(TableEntry entry, ProgramManager programManager, String gapTitle) {
+ void onBind(TableEntry entry, ProgramGuide programGuide, String gapTitle) {
if (DEBUG) {
Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry);
}
- ((ProgramItemView) itemView).setValues(entry, programManager.getSelectedGenreId(),
- programManager.getFromUtcMillis(), programManager.getToUtcMillis(), gapTitle);
+ ProgramManager programManager = programGuide.getProgramManager();
+ ((ProgramItemView) itemView).setValues(programGuide, entry,
+ programManager.getSelectedGenreId(), programManager.getFromUtcMillis(),
+ programManager.getToUtcMillis(), gapTitle);
}
- public void onUnbind() {
+ void onUnbind() {
((ProgramItemView) itemView).clearValues();
}
}
diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java
index e3d919df..4ec3f77e 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;
@@ -68,107 +68,6 @@ public class ProgramManager {
private long mFromUtcMillis;
private long mToUtcMillis;
- /**
- * Entry for program guide table. An "entry" can be either an actual program or a gap between
- * programs. This is needed for {@link ProgramListAdapter} because
- * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items.
- */
- public static class TableEntry {
- /** Channel ID which this entry is included. */
- public final long channelId;
-
- /** Program corresponding to the entry. {@code null} means that this entry is a gap. */
- public final Program program;
-
- public final ScheduledRecording scheduledRecording;
-
- /** Start time of entry in UTC milliseconds. */
- public final long entryStartUtcMillis;
-
- /** End time of entry in UTC milliseconds */
- public final long entryEndUtcMillis;
-
- private final boolean mIsBlocked;
-
- private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
- this(channelId, null, startUtcMillis, endUtcMillis, false);
- }
-
- private TableEntry(long channelId, long startUtcMillis, long endUtcMillis,
- boolean blocked) {
- this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
- }
-
- private TableEntry(long channelId, Program program, long entryStartUtcMillis,
- long entryEndUtcMillis, boolean isBlocked) {
- this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
- }
-
- private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording,
- long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) {
- this.channelId = channelId;
- this.program = program;
- this.scheduledRecording = scheduledRecording;
- this.entryStartUtcMillis = entryStartUtcMillis;
- this.entryEndUtcMillis = entryEndUtcMillis;
- mIsBlocked = isBlocked;
- }
-
- /**
- * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}.
- */
- public long getId() {
- // using a negative entryEndUtcMillis keeps it from conflicting with program Id
- return program != null ? program.getId() : -entryEndUtcMillis;
- }
-
- /**
- * Returns true if this is a gap.
- */
- public boolean isGap() {
- return !Program.isValid(program);
- }
-
- /**
- * Returns true if this channel is blocked.
- */
- public boolean isBlocked() {
- return mIsBlocked;
- }
-
- /**
- * Returns true if this program is on the air.
- */
- public boolean isCurrentProgram() {
- long current = System.currentTimeMillis();
- return entryStartUtcMillis <= current && entryEndUtcMillis > current;
- }
-
- /**
- * Returns if this program has the genre.
- */
- public boolean hasGenre(int genreId) {
- return !isGap() && program.hasGenre(genreId);
- }
-
- /**
- * Returns the width of table entry, in pixels.
- */
- public int getWidth() {
- return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
- }
-
- @Override
- public String toString() {
- return "TableEntry{"
- + "hashCode=" + hashCode()
- + ", channelId=" + channelId
- + ", program=" + program
- + ", startTime=" + Utils.toTimeString(entryStartUtcMillis)
- + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}";
- }
- }
-
private List<Channel> mChannels = new ArrayList<>();
private final Map<Long, List<TableEntry>> mChannelIdEntriesMap = new HashMap<>();
private final List<List<Channel>> mGenreChannelList = new ArrayList<>();
@@ -293,7 +192,7 @@ public class ProgramManager {
mDvrScheduleManager = dvrScheduleManager;
}
- public void programGuideVisibilityChanged(boolean visible) {
+ void programGuideVisibilityChanged(boolean visible) {
mProgramDataManager.setPauseProgramUpdate(visible);
if (visible) {
mChannelDataManager.addListener(mChannelDataManagerListener);
@@ -325,87 +224,51 @@ public class ProgramManager {
/**
* Adds a {@link Listener}.
*/
- public void addListener(Listener listener) {
+ void addListener(Listener listener) {
mListeners.add(listener);
}
/**
* Registers a listener to be invoked when table entries are updated.
*/
- public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
+ void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.add(listener);
}
/**
* Registers a listener to be invoked when a table entry is changed.
*/
- public void addTableEntryChangedListener(TableEntryChangedListener listener) {
+ void addTableEntryChangedListener(TableEntryChangedListener listener) {
mTableEntryChangedListeners.add(listener);
}
/**
* Removes a {@link Listener}.
*/
- public void removeListener(Listener listener) {
+ void removeListener(Listener listener) {
mListeners.remove(listener);
}
/**
* Removes a previously installed table entries update listener.
*/
- public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
+ void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) {
mTableEntriesUpdatedListeners.remove(listener);
}
/**
* Removes a previously installed table entry changed listener.
*/
- public void removeTableEntryChangedListener(TableEntryChangedListener listener) {
+ void removeTableEntryChangedListener(TableEntryChangedListener listener) {
mTableEntryChangedListeners.remove(listener);
}
/**
- * Build genre filters based on the current programs.
- * This categories channels by its current program's canonical genres
- * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list
- * with built channel list.
- * This is expected to be called whenever program guide is shown.
- */
- public void buildGenreFilters() {
- if (DEBUG) Log.d(TAG, "buildGenreFilters");
-
- mGenreChannelList.clear();
- for (int i = 0; i < GenreItems.getGenreCount(); i++) {
- mGenreChannelList.add(new ArrayList<>());
- }
- for (Channel channel : mChannels) {
- // TODO: Use programs in visible area instead of using current programs only.
- Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId());
- if (currentProgram != null && currentProgram.getCanonicalGenres() != null) {
- for (String genre : currentProgram.getCanonicalGenres()) {
- mGenreChannelList.get(GenreItems.getId(genre)).add(channel);
- }
- }
- }
- mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels);
- mFilteredGenreIds.clear();
- mFilteredGenreIds.add(0);
- for (int i = 1; i < GenreItems.getGenreCount(); i++) {
- if (mGenreChannelList.get(i).size() > 0) {
- mFilteredGenreIds.add(i);
- }
- }
- mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
- mFilteredChannels = mChannels;
- notifyGenresUpdated();
- }
-
- /**
* Resets channel list with given genre.
* Caller should call {@link #buildGenreFilters()} prior to call this API to make
* This notifies channel updates to listeners.
*/
- public void resetChannelListWithGenre(int genreId) {
+ void resetChannelListWithGenre(int genreId) {
if (genreId == mSelectedGenreId) {
return;
}
@@ -422,13 +285,154 @@ public class ProgramManager {
}
/**
+ * Update the initial time range to manage. It updates program entries and genre as well.
+ */
+ void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) {
+ mStartUtcMillis = startUtcMillis;
+ if (endUtcMillis > mEndUtcMillis) {
+ mEndUtcMillis = endUtcMillis;
+ }
+
+ mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
+ updateChannels(true);
+ setTimeRange(startUtcMillis, endUtcMillis);
+ }
+
+
+ /**
+ * Shifts the time range by the given time. Also makes ProgramGuide scroll the views.
+ */
+ void shiftTime(long timeMillisToScroll) {
+ long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
+ long toUtcMillis = mToUtcMillis + timeMillisToScroll;
+ if (fromUtcMillis < mStartUtcMillis) {
+ fromUtcMillis = mStartUtcMillis;
+ toUtcMillis += mStartUtcMillis - fromUtcMillis;
+ }
+ if (toUtcMillis > mEndUtcMillis) {
+ fromUtcMillis -= toUtcMillis - mEndUtcMillis;
+ toUtcMillis = mEndUtcMillis;
+ }
+ setTimeRange(fromUtcMillis, toUtcMillis);
+ }
+
+ /**
+ * Returned the scrolled(shifted) time in milliseconds.
+ */
+ long getShiftedTime() {
+ return mFromUtcMillis - mStartUtcMillis;
+ }
+
+ /**
+ * Returns the start time set by {@link #updateInitialTimeRange}.
+ */
+ long getStartTime() {
+ return mStartUtcMillis;
+ }
+
+ /**
+ * Returns the program index of the program with {@code entryId} or -1 if not found.
+ */
+ int getProgramIdIndex(long channelId, long entryId) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
+ if (entries != null) {
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getId() == entryId) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the program index of the program at {@code time} or -1 if not found.
+ */
+ int getProgramIndexAtTime(long channelId, long time) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
+ for (int i = 0; i < entries.size(); ++i) {
+ TableEntry entry = entries.get(i);
+ if (entry.entryStartUtcMillis <= time
+ && time < entry.entryEndUtcMillis) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the start time of currently managed time range, in UTC millisecond.
+ */
+ long getFromUtcMillis() {
+ return mFromUtcMillis;
+ }
+
+ /**
+ * Returns the end time of currently managed time range, in UTC millisecond.
+ */
+ long getToUtcMillis() {
+ return mToUtcMillis;
+ }
+
+ /**
+ * Returns the number of the currently managed channels.
+ */
+ int getChannelCount() {
+ return mFilteredChannels.size();
+ }
+
+ /**
+ * Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels.
+ * Returns {@code null} if such a channel is not found.
+ */
+ Channel getChannel(int channelIndex) {
+ if (channelIndex < 0 || channelIndex >= getChannelCount()) {
+ return null;
+ }
+ return mFilteredChannels.get(channelIndex);
+ }
+
+ /**
+ * Returns the index of provided {@link Channel} within the currently managed channels.
+ * Returns -1 if such a channel is not found.
+ */
+ int getChannelIndex(Channel channel) {
+ return mFilteredChannels.indexOf(channel);
+ }
+
+ /**
+ * Returns the index of channel with {@code channelId} within the currently managed channels.
+ * Returns -1 if such a channel is not found.
+ */
+ int getChannelIndex(long channelId) {
+ return getChannelIndex(mChannelDataManager.getChannel(channelId));
+ }
+
+ /**
+ * Returns the number of "entries", which lies within the currently managed time range, for a
+ * given {@code channelId}.
+ */
+ int getTableEntryCount(long channelId) {
+ return mChannelIdEntriesMap.get(channelId).size();
+ }
+
+ /**
+ * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of
+ * entries within the currently managed time range. Returned {@link Program} can be a dummy one
+ * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
+ */
+ TableEntry getTableEntry(long channelId, int index) {
+ return mChannelIdEntriesMap.get(channelId).get(index);
+ }
+
+ /**
* Returns list genre ID's which has a channel.
*/
- public List<Integer> getFilteredGenreIds() {
+ List<Integer> getFilteredGenreIds() {
return mFilteredGenreIds;
}
- public int getSelectedGenreId() {
+ int getSelectedGenreId() {
return mSelectedGenreId;
}
@@ -439,11 +443,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,46 +508,41 @@ public class ProgramManager {
}
}
}
-
- notifyTableEntriesUpdated();
- buildGenreFilters();
- }
-
- private void notifyGenresUpdated() {
- for (Listener listener : mListeners) {
- listener.onGenresUpdated();
- }
}
- private void notifyChannelsUpdated() {
- for (Listener listener : mListeners) {
- listener.onChannelsUpdated();
- }
- }
+ /**
+ * Build genre filters based on the current programs.
+ * This categories channels by its current program's canonical genres
+ * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list
+ * with built channel list.
+ * This is expected to be called whenever program guide is shown.
+ */
+ private void buildGenreFilters() {
+ if (DEBUG) Log.d(TAG, "buildGenreFilters");
- private void notifyTimeRangeUpdated() {
- for (Listener listener : mListeners) {
- listener.onTimeRangeUpdated();
+ mGenreChannelList.clear();
+ for (int i = 0; i < GenreItems.getGenreCount(); i++) {
+ mGenreChannelList.add(new ArrayList<>());
}
- }
-
- private void notifyTableEntriesUpdated() {
- for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
- listener.onTableEntriesUpdated();
+ for (Channel channel : mChannels) {
+ Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId());
+ if (currentProgram != null && currentProgram.getCanonicalGenres() != null) {
+ for (String genre : currentProgram.getCanonicalGenres()) {
+ mGenreChannelList.get(GenreItems.getId(genre)).add(channel);
+ }
+ }
}
- }
-
- private void notifyTableEntryUpdated(TableEntry entry) {
- for (TableEntryChangedListener listener : mTableEntryChangedListeners) {
- listener.onTableEntryChanged(entry);
+ mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels);
+ mFilteredGenreIds.clear();
+ mFilteredGenreIds.add(0);
+ for (int i = 1; i < GenreItems.getGenreCount(); i++) {
+ if (mGenreChannelList.get(i).size() > 0) {
+ mFilteredGenreIds.add(i);
+ }
}
- }
-
- private void updateEntry(TableEntry old, TableEntry newEntry) {
- List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId);
- int index = entries.indexOf(old);
- entries.set(index, newEntry);
- notifyTableEntryUpdated(newEntry);
+ mSelectedGenreId = GenreItems.ID_ALL_CHANNELS;
+ mFilteredChannels = mChannels;
+ notifyGenresUpdated();
}
@Nullable
@@ -551,32 +563,11 @@ public class ProgramManager {
return null;
}
- /**
- * Returns the start time of currently managed time range, in UTC millisecond.
- */
- public long getFromUtcMillis() {
- return mFromUtcMillis;
- }
-
- /**
- * Returns the end time of currently managed time range, in UTC millisecond.
- */
- public long getToUtcMillis() {
- return mToUtcMillis;
- }
-
- /**
- * Update the initial time range to manage. It updates program entries and genre as well.
- */
- public void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) {
- mStartUtcMillis = startUtcMillis;
- if (endUtcMillis > mEndUtcMillis) {
- mEndUtcMillis = endUtcMillis;
- }
-
- mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis);
- updateChannels(true);
- setTimeRange(startUtcMillis, endUtcMillis);
+ private void updateEntry(TableEntry old, TableEntry newEntry) {
+ List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId);
+ int index = entries.indexOf(old);
+ entries.set(index, newEntry);
+ notifyTableEntryUpdated(newEntry);
}
private void setTimeRange(long fromUtcMillis, long toUtcMillis) {
@@ -592,57 +583,6 @@ public class ProgramManager {
}
}
- /**
- * Returns the number of the currently managed channels.
- */
- public int getChannelCount() {
- return mFilteredChannels.size();
- }
-
- /**
- * Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels.
- * Returns {@code null} if such a channel is not found.
- */
- public Channel getChannel(int channelIndex) {
- if (channelIndex < 0 || channelIndex >= getChannelCount()) {
- return null;
- }
- return mFilteredChannels.get(channelIndex);
- }
-
- /**
- * Returns the index of provided {@link Channel} within the currently managed channels.
- * Returns -1 if such a channel is not found.
- */
- public int getChannelIndex(Channel channel) {
- return mFilteredChannels.indexOf(channel);
- }
-
- /**
- * Returns the index of channel with {@code channelId} within the currently managed channels.
- * Returns -1 if such a channel is not found.
- */
- public int getChannelIndex(long channelId) {
- return getChannelIndex(mChannelDataManager.getChannel(channelId));
- }
-
- /**
- * Returns the number of "entries", which lies within the currently managed time range, for a
- * given {@code channelId}.
- */
- public int getTableEntryCount(long channelId) {
- return mChannelIdEntriesMap.get(channelId).size();
- }
-
- /**
- * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of
- * entries within the currently managed time range. Returned {@link Program} can be a dummy one
- * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs.
- */
- public TableEntry getTableEntry(long channelId, int index) {
- return mChannelIdEntriesMap.get(channelId).get(index);
- }
-
private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) {
List<TableEntry> entries = new ArrayList<>();
boolean channelLocked = parentalControlsEnabled
@@ -690,89 +630,159 @@ public class ProgramManager {
return entries;
}
- public interface Listener {
- void onGenresUpdated();
- void onChannelsUpdated();
- void onTimeRangeUpdated();
+ private void notifyGenresUpdated() {
+ for (Listener listener : mListeners) {
+ listener.onGenresUpdated();
+ }
}
- public interface TableEntriesUpdatedListener {
- void onTableEntriesUpdated();
+ private void notifyChannelsUpdated() {
+ for (Listener listener : mListeners) {
+ listener.onChannelsUpdated();
+ }
}
- public interface TableEntryChangedListener {
- void onTableEntryChanged(TableEntry entry);
+ private void notifyTimeRangeUpdated() {
+ for (Listener listener : mListeners) {
+ listener.onTimeRangeUpdated();
+ }
}
- public static class ListenerAdapter implements Listener {
- @Override
- public void onGenresUpdated() { }
-
- @Override
- public void onChannelsUpdated() { }
+ private void notifyTableEntriesUpdated() {
+ for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) {
+ listener.onTableEntriesUpdated();
+ }
+ }
- @Override
- public void onTimeRangeUpdated() { }
+ private void notifyTableEntryUpdated(TableEntry entry) {
+ for (TableEntryChangedListener listener : mTableEntryChangedListeners) {
+ listener.onTableEntryChanged(entry);
+ }
}
/**
- * Shifts the time range by the given time. Also makes ProgramGuide scroll the views.
+ * Entry for program guide table. An "entry" can be either an actual program or a gap between
+ * programs. This is needed for {@link ProgramListAdapter} because
+ * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items.
*/
- public void shiftTime(long timeMillisToScroll) {
- long fromUtcMillis = mFromUtcMillis + timeMillisToScroll;
- long toUtcMillis = mToUtcMillis + timeMillisToScroll;
- if (fromUtcMillis < mStartUtcMillis) {
- fromUtcMillis = mStartUtcMillis;
- toUtcMillis += mStartUtcMillis - fromUtcMillis;
+ static class TableEntry {
+ /** Channel ID which this entry is included. */
+ final long channelId;
+
+ /** Program corresponding to the entry. {@code null} means that this entry is a gap. */
+ final Program program;
+
+ final ScheduledRecording scheduledRecording;
+
+ /** Start time of entry in UTC milliseconds. */
+ final long entryStartUtcMillis;
+
+ /** End time of entry in UTC milliseconds */
+ final long entryEndUtcMillis;
+
+ private final boolean mIsBlocked;
+
+ private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) {
+ this(channelId, null, startUtcMillis, endUtcMillis, false);
}
- if (toUtcMillis > mEndUtcMillis) {
- fromUtcMillis -= toUtcMillis - mEndUtcMillis;
- toUtcMillis = mEndUtcMillis;
+
+ private TableEntry(long channelId, long startUtcMillis, long endUtcMillis,
+ boolean blocked) {
+ this(channelId, null, null, startUtcMillis, endUtcMillis, blocked);
+ }
+
+ private TableEntry(long channelId, Program program, long entryStartUtcMillis,
+ long entryEndUtcMillis, boolean isBlocked) {
+ this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked);
+ }
+
+ private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording,
+ long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) {
+ this.channelId = channelId;
+ this.program = program;
+ this.scheduledRecording = scheduledRecording;
+ this.entryStartUtcMillis = entryStartUtcMillis;
+ this.entryEndUtcMillis = entryEndUtcMillis;
+ mIsBlocked = isBlocked;
+ }
+
+ /**
+ * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}.
+ */
+ long getId() {
+ // using a negative entryEndUtcMillis keeps it from conflicting with program Id
+ return program != null ? program.getId() : -entryEndUtcMillis;
+ }
+
+ /**
+ * Returns true if this is a gap.
+ */
+ boolean isGap() {
+ return !Program.isValid(program);
+ }
+
+ /**
+ * Returns true if this channel is blocked.
+ */
+ boolean isBlocked() {
+ return mIsBlocked;
+ }
+
+ /**
+ * Returns true if this program is on the air.
+ */
+ boolean isCurrentProgram() {
+ long current = System.currentTimeMillis();
+ return entryStartUtcMillis <= current && entryEndUtcMillis > current;
+ }
+
+ /**
+ * Returns if this program has the genre.
+ */
+ boolean hasGenre(int genreId) {
+ return !isGap() && program.hasGenre(genreId);
+ }
+
+ /**
+ * Returns the width of table entry, in pixels.
+ */
+ int getWidth() {
+ return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis);
+ }
+
+ @Override
+ public String toString() {
+ return "TableEntry{"
+ + "hashCode=" + hashCode()
+ + ", channelId=" + channelId
+ + ", program=" + program
+ + ", startTime=" + Utils.toTimeString(entryStartUtcMillis)
+ + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}";
}
- setTimeRange(fromUtcMillis, toUtcMillis);
}
- /**
- * Returned the scrolled(shifted) time in milliseconds.
- */
- public long getShiftedTime() {
- return mFromUtcMillis - mStartUtcMillis;
+ interface Listener {
+ void onGenresUpdated();
+ void onChannelsUpdated();
+ void onTimeRangeUpdated();
}
- /**
- * Returns the start time set by {@link #updateInitialTimeRange}.
- */
- public long getStartTime() {
- return mStartUtcMillis;
+ interface TableEntriesUpdatedListener {
+ void onTableEntriesUpdated();
}
- /**
- * Returns the program index of the program with {@code entryId} or -1 if not found.
- */
- public int getProgramIdIndex(long channelId, long entryId) {
- List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
- if (entries != null) {
- for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).getId() == entryId) {
- return i;
- }
- }
- }
- return -1;
+ interface TableEntryChangedListener {
+ void onTableEntryChanged(TableEntry entry);
}
- /**
- * Returns the program index of the program at {@code time} or -1 if not found.
- */
- public int getProgramIndexAtTime(long channelId, long time) {
- List<TableEntry> entries = mChannelIdEntriesMap.get(channelId);
- for (int i = 0; i < entries.size(); ++i) {
- TableEntry entry = entries.get(i);
- if (entry.entryStartUtcMillis <= time
- && time < entry.entryEndUtcMillis) {
- return i;
- }
- }
- return -1;
+ static class ListenerAdapter implements Listener {
+ @Override
+ public void onGenresUpdated() { }
+
+ @Override
+ public void onChannelsUpdated() { }
+
+ @Override
+ public void onTimeRangeUpdated() { }
}
}
diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java
index 2c98ab2d..fefc724c 100644
--- a/src/com/android/tv/guide/ProgramRow.java
+++ b/src/com/android/tv/guide/ProgramRow.java
@@ -21,9 +21,11 @@ import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Range;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import com.android.tv.MainActivity;
import com.android.tv.data.Channel;
import com.android.tv.guide.ProgramManager.TableEntry;
import com.android.tv.util.Utils;
@@ -37,6 +39,7 @@ public class ProgramRow extends TimelineGridView {
private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1);
private static final long HALF_HOUR_MILLIS = ONE_HOUR_MILLIS / 2;
+ private ProgramGuide mProgramGuide;
private ProgramManager mProgramManager;
private boolean mKeepFocusToCurrentProgram;
@@ -44,8 +47,8 @@ public class ProgramRow extends TimelineGridView {
interface ChildFocusListener {
/**
- * Is called after focus is moved. It used {@link ChildFocusListener#isChild} to decide if
- * old and new focuses are listener's children.
+ * Is called after focus is moved. Caller should check if old and new focuses are
+ * listener's children.
* See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}.
*/
void onChildFocus(View oldFocus, View newFocus);
@@ -213,7 +216,6 @@ public class ProgramRow extends TimelineGridView {
// so give focus back in onChildAttachedToWindow().
mKeepFocusToCurrentProgram = true;
}
- // TODO: Try to keep focus for non-current program.
}
super.onChildDetachedFromWindow(child);
}
@@ -237,16 +239,18 @@ public class ProgramRow extends TimelineGridView {
@Override
public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
- // Give focus to the current program by default.
- // Note that this logic is used only if requestFocus() is called to the ProgramRow,
- // so focus finding logic will not be blocked by this.
- View currentProgram = getCurrentProgramView();
- if (currentProgram != null) {
- return currentProgram.requestFocus();
+ ProgramGrid programGrid = mProgramGuide.getProgramGrid();
+
+ // Give focus according to the previous focused range
+ Range<Integer> focusRange = programGrid.getFocusRange();
+ View nextFocus = GuideUtils.findNextFocusedProgram(this, focusRange.getLower(),
+ focusRange.getUpper(), programGrid.isKeepCurrentProgramFocused());
+
+ if (nextFocus != null) {
+ return nextFocus.requestFocus();
}
if (DEBUG) Log.d(TAG, "onRequestFocusInDescendants");
-
boolean result = super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
if (!result) {
// The default focus search logic of LeanbackLibrary is sometimes failed.
@@ -276,10 +280,11 @@ public class ProgramRow extends TimelineGridView {
}
/**
- * Sets the instance of {@link ProgramManager}
+ * Sets the instance of {@link ProgramGuide}
*/
- public void setProgramManager(ProgramManager programManager) {
- mProgramManager = programManager;
+ public void setProgramGuide(ProgramGuide programGuide) {
+ mProgramGuide = programGuide;
+ mProgramManager = programGuide.getProgramManager();
}
/**
@@ -300,7 +305,7 @@ public class ProgramRow extends TimelineGridView {
.scrollToPositionWithOffset(position, offset);
// Workaround to b/31598505. When a program's duration is too long,
// RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset().
- // Therefore we have to update children's visible areas by ourselves in theis case.
+ // Therefore we have to update children's visible areas by ourselves in this case.
// Since scrollToPositionWithOffset() will call requestLayout(), we can listen to this
// behavior to ensure program items' visible areas are correctly updated after layouts
// are adjusted, i.e., scrolling is over.
diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java
index e4a67972..99f853b1 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;
@@ -73,7 +75,7 @@ import java.util.List;
/**
* Adapts the {@link ProgramListAdapter} list to the body of the program guide table.
*/
-public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder>
+class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowViewHolder>
implements ProgramManager.TableEntryChangedListener {
private static final String TAG = "ProgramTableAdapter";
private static final boolean DEBUG = false;
@@ -112,8 +114,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
private final int mDvrPaddingStartWithTrack;
private final int mDvrPaddingStartWithOutTrack;
- public ProgramTableAdapter(Context context, ProgramManager programManager,
- ProgramGuide programGuide) {
+ ProgramTableAdapter(Context context, ProgramGuide programGuide) {
mContext = context;
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
@@ -125,8 +126,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mDvrManager = null;
mDvrDataManager = null;
}
- mProgramManager = programManager;
mProgramGuide = programGuide;
+ mProgramManager = programGuide.getProgramManager();
Resources res = context.getResources();
mChannelLogoWidth = res.getDimensionPixelSize(
@@ -193,7 +194,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mProgramListAdapters.clear();
for (int i = 0; i < mProgramManager.getChannelCount(); i++) {
ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(),
- mProgramManager, i);
+ mProgramGuide, i);
mProgramManager.addTableEntriesUpdatedListener(listAdapter);
mProgramListAdapters.add(listAdapter);
}
@@ -211,12 +212,12 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
@Override
- public void onBindViewHolder(ProgramRowHolder holder, int position) {
+ public void onBindViewHolder(ProgramRowViewHolder holder, int position) {
holder.onBind(position);
}
@Override
- public void onBindViewHolder(ProgramRowHolder holder, int position, List<Object> payloads) {
+ public void onBindViewHolder(ProgramRowViewHolder holder, int position, List<Object> payloads) {
if (!payloads.isEmpty()) {
holder.updateDetailView();
} else {
@@ -225,11 +226,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
@Override
- public ProgramRowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public ProgramRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row);
programRow.setRecycledViewPool(mRecycledViewPool);
- return new ProgramRowHolder(itemView);
+ return new ProgramRowViewHolder(itemView);
}
@Override
@@ -241,18 +242,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
notifyItemChanged(channelIndex, true);
}
- @Override
- public void onViewAttachedToWindow(ProgramRowHolder holder) {
- holder.onAttachedToWindow();
- }
-
- @Override
- public void onViewDetachedFromWindow(ProgramRowHolder holder) {
- holder.onDetachedFromWindow();
- }
-
- // TODO: make it static
- public class ProgramRowHolder extends RecyclerView.ViewHolder
+ class ProgramRowViewHolder extends RecyclerView.ViewHolder
implements ProgramRow.ChildFocusListener {
private final ViewGroup mContainer;
@@ -269,6 +259,12 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
}
};
+ private final Runnable mUpdateDetailViewRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateDetailView();
+ }
+ };
private final RecyclerView.OnScrollListener mOnScrollListener =
new RecyclerView.OnScrollListener() {
@@ -282,8 +278,9 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- onChildFocus(isChild(oldFocus) ? oldFocus : null,
- isChild(newFocus) ? newFocus : null);
+ onChildFocus(
+ GuideUtils.isDescendant(mContainer, oldFocus) ? oldFocus : null,
+ GuideUtils.isDescendant(mContainer, newFocus) ? newFocus : null);
}
};
@@ -312,11 +309,38 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
private final ImageView mInputLogoView;
private boolean mIsInputLogoVisible;
+ private AccessibilityStateChangeListener mAccessibilityStateChangeListener =
+ new AccessibilityManager.AccessibilityStateChangeListener() {
+ @Override
+ public void onAccessibilityStateChanged(boolean enable) {
+ enable &= !TvCommonUtils.isRunningInTest();
+ mDetailView.setFocusable(enable);
+ mChannelHeaderView.setFocusable(enable);
+ }
+ };
- public ProgramRowHolder(View itemView) {
+ ProgramRowViewHolder(View itemView) {
super(itemView);
mContainer = (ViewGroup) itemView;
+ mContainer.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ mContainer.getViewTreeObserver()
+ .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ mAccessibilityManager.addAccessibilityStateChangeListener(
+ mAccessibilityStateChangeListener);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ mContainer.getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
+ mAccessibilityManager.removeAccessibilityStateChangeListener(
+ mAccessibilityStateChangeListener);
+ }
+ });
mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row);
mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail);
@@ -339,23 +363,18 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo);
mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block);
mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo);
- mDetailView.setFocusable(mAccessibilityManager.isEnabled());
- mChannelHeaderView.setFocusable(mAccessibilityManager.isEnabled());
- mAccessibilityManager.addAccessibilityStateChangeListener(
- new AccessibilityManager.AccessibilityStateChangeListener() {
- @Override
- public void onAccessibilityStateChanged(boolean enable) {
- mDetailView.setFocusable(enable);
- mChannelHeaderView.setFocusable(enable);
- }
- });
+
+ boolean accessibilityEnabled = mAccessibilityManager.isEnabled()
+ && !TvCommonUtils.isRunningInTest();
+ mDetailView.setFocusable(accessibilityEnabled);
+ mChannelHeaderView.setFocusable(accessibilityEnabled);
}
public void onBind(int position) {
onBindChannel(mProgramManager.getChannel(position));
mProgramRow.swapAdapter(mProgramListAdapters.get(position), true);
- mProgramRow.setProgramManager(mProgramManager);
+ mProgramRow.setProgramGuide(mProgramGuide);
mProgramRow.setChannel(mProgramManager.getChannel(position));
mProgramRow.setChildFocusListener(this);
mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset());
@@ -416,24 +435,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
}
- public boolean isChild(View view) {
- if (view == null) {
- return false;
- }
- for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
- if (p == mContainer) {
- return true;
- }
- }
- return false;
- }
-
@Override
public void onChildFocus(View oldFocus, View newFocus) {
if (newFocus == null) {
return;
- }
- // When the accessibility service is enabled, focus might be put on channel's header or
+ } // When the accessibility service is enabled, focus might be put on channel's header or
// detail view, besides program items.
if (newFocus == mChannelHeaderView) {
mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry();
@@ -443,7 +449,15 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry();
}
if (oldFocus == null) {
- updateDetailView();
+ // Focus moved from other row.
+ if (mProgramGuide.getProgramGrid().isInLayout()) {
+ // We need to post runnable to avoid updating detail view when
+ // the recycler view is in layout, which may cause detail view not
+ // laid out according to the updated contents.
+ mHandler.post(mUpdateDetailViewRunnable);
+ } else {
+ updateDetailView();
+ }
return;
}
@@ -508,16 +522,6 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
});
}
- private void onAttachedToWindow() {
- mContainer.getViewTreeObserver()
- .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
- }
-
- private void onDetachedFromWindow() {
- mContainer.getViewTreeObserver()
- .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
- }
-
private void updateDetailView() {
if (mSelectedEntry == null) {
// The view holder is never on focus before.
@@ -556,10 +560,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
program.getStartTimeUtcMillis(),
program.getEndTimeUtcMillis(), false));
- boolean trackMetaDataVisible = false;
- trackMetaDataVisible |=
- updateTextView(mAspectRatioView, Utils.getAspectRatioString(
- program.getVideoWidth(), program.getVideoHeight()));
+ boolean trackMetaDataVisible = updateTextView(mAspectRatioView, Utils
+ .getAspectRatioString(program.getVideoWidth(), program.getVideoHeight()));
int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize(
program.getVideoWidth(), program.getVideoHeight());
@@ -658,7 +660,9 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
if (TextUtils.isEmpty(name)) {
return mContext.getString(R.string.program_guide_content_locked);
} else {
- return mContext.getString(R.string.program_guide_content_locked_format, name);
+ return TvContentRating.UNRATED.equals(blockedRating)
+ ? mContext.getString(R.string.program_guide_content_locked_unrated)
+ : mContext.getString(R.string.program_guide_content_locked_format, name);
}
}
@@ -666,7 +670,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
* Update tv input logo. It should be called when the visible child item in ProgramGrid
* changed.
*/
- public void updateInputLogo(int lastPosition, boolean forceShow) {
+ void updateInputLogo(int lastPosition, boolean forceShow) {
if (mChannel == null) {
mInputLogoView.setVisibility(View.GONE);
mIsInputLogoVisible = false;
@@ -731,7 +735,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
mInputLogoView.setVisibility(View.VISIBLE);
}
- private void updateCriticScoreView(ProgramRowHolder holder, final long programId,
+ private void updateCriticScoreView(ProgramRowViewHolder holder, final long programId,
CriticScore criticScore, View view) {
TextView criticScoreSource = (TextView) view.findViewById(R.id.critic_score_source);
TextView criticScoreText = (TextView) view.findViewById(R.id.critic_score_score);
@@ -759,11 +763,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
}
}
- private static ImageLoaderCallback<ProgramRowHolder> createCriticScoreLogoCallback(
- ProgramRowHolder holder, final long programId, ImageView logoView) {
- return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ private static ImageLoaderCallback<ProgramRowViewHolder> createCriticScoreLogoCallback(
+ ProgramRowViewHolder holder, final long programId, ImageView logoView) {
+ return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
@Override
- public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logoImage) {
+ public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logoImage) {
if (logoImage == null || holder.mSelectedEntry == null
|| holder.mSelectedEntry.program == null
|| holder.mSelectedEntry.program.getId() != programId) {
@@ -776,11 +780,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
};
}
- private static ImageLoaderCallback<ProgramRowHolder> createProgramPosterArtCallback(
- ProgramRowHolder holder, final Program program) {
- return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ private static ImageLoaderCallback<ProgramRowViewHolder> createProgramPosterArtCallback(
+ ProgramRowViewHolder holder, final Program program) {
+ return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
@Override
- public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap posterArt) {
+ public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap posterArt) {
if (posterArt == null || holder.mSelectedEntry == null
|| holder.mSelectedEntry.program == null) {
return;
@@ -794,11 +798,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
};
}
- private static ImageLoaderCallback<ProgramRowHolder> createChannelLogoLoadedCallback(
- ProgramRowHolder holder, final long channelId) {
- return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ private static ImageLoaderCallback<ProgramRowViewHolder> createChannelLogoLoadedCallback(
+ ProgramRowViewHolder holder, final long channelId) {
+ return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
@Override
- public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) {
+ public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) {
if (logo == null || holder.mChannel == null
|| holder.mChannel.getId() != channelId) {
return;
@@ -808,11 +812,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte
};
}
- private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback(
- final TvInputInfo info, ProgramRowHolder holder) {
- return new ImageLoaderCallback<ProgramRowHolder>(holder) {
+ private static ImageLoaderCallback<ProgramRowViewHolder> createTvInputLogoLoadedCallback(
+ final TvInputInfo info, ProgramRowViewHolder holder) {
+ return new ImageLoaderCallback<ProgramRowViewHolder>(holder) {
@Override
- public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) {
+ public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) {
if (logo != null && holder.mChannel != null && info.getId()
.equals(holder.mChannel.getInputId())) {
holder.updateInputLogoInternal(logo);
diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java
index 868fed46..d9e96a40 100644
--- a/src/com/android/tv/guide/TimeListAdapter.java
+++ b/src/com/android/tv/guide/TimeListAdapter.java
@@ -16,6 +16,7 @@
package com.android.tv.guide;
+import android.content.Context;
import android.content.res.Resources;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateFormat;
@@ -25,26 +26,40 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.R;
+import com.android.tv.util.Utils;
import java.util.Date;
+import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
* Adapts the time range from {@link ProgramManager} to the timeline header row of the program
* guide table.
*/
-public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> {
+class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> {
private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30);
+
+ // Ex. 3:00 AM
+ private static final String TIME_PATTERN_SAME_DAY = "h:mm a";
+ // Ex. Oct 21, 3:00 AM
+ private static final String TIME_PATTERN_DIFFERENT_DAY = "MMM d, h:mm a";
+
private static int sRowHeaderOverlapping;
// Nearest half hour at or before the start time.
private long mStartUtcMs;
+ private final String mTimePatternSameDay;
+ private final String mTimePatternDifferentDay;
- public TimeListAdapter(Resources res) {
+ TimeListAdapter(Resources res) {
if (sRowHeaderOverlapping == 0) {
sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset(
R.dimen.program_guide_table_header_row_overlap));
}
+ Locale locale = res.getConfiguration().locale;
+ mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY);
+ mTimePatternDifferentDay =
+ DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_DIFFERENT_DAY);
}
public void update(long startTimeMs) {
@@ -68,10 +83,14 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi
long endTime = startTime + TIME_UNIT_MS;
View itemView = holder.itemView;
-
- TextView textView = (TextView) itemView.findViewById(R.id.time);
- String time = DateFormat.getTimeFormat(itemView.getContext()).format(new Date(startTime));
- textView.setText(time);
+ Date timeDate = new Date(startTime);
+ String timeString;
+ if (Utils.isInGivenDay(System.currentTimeMillis(), startTime)) {
+ timeString = DateFormat.format(mTimePatternSameDay, timeDate).toString();
+ } else {
+ timeString = DateFormat.format(mTimePatternDifferentDay, timeDate).toString();
+ }
+ ((TextView) itemView.findViewById(R.id.time)).setText(timeString);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
lp.width = GuideUtils.convertMillisToPixel(startTime, endTime);
@@ -90,8 +109,8 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi
return new TimeViewHolder(itemView);
}
- public static class TimeViewHolder extends RecyclerView.ViewHolder {
- public TimeViewHolder(View itemView) {
+ static class TimeViewHolder extends RecyclerView.ViewHolder {
+ TimeViewHolder(View itemView) {
super(itemView);
}
}
diff --git a/src/com/android/tv/license/License.java b/src/com/android/tv/license/License.java
new file mode 100644
index 00000000..c1cbd09e
--- /dev/null
+++ b/src/com/android/tv/license/License.java
@@ -0,0 +1,112 @@
+/*
+ * 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.license;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Container class to store the name of a library and the filename of its associated license file.
+ */
+public final class License implements Comparable<License>, Parcelable {
+ // Name of the third-party library.
+ private final String mLibraryName;
+ // Byte offset in the file to the start of the license text.
+ private final long mLicenseOffset;
+ // Byte length of the license text.
+ private final int mLicenseLength;
+ // Path to the archive that has bundled licenses.
+ // Empty string if the license is bundled in the apk itself.
+ private final String mPath;
+
+ /**
+ * Create an object representing a stored license. The text for all licenses is stored in a
+ * single file, so the offset and length describe this license's position within the file.
+ *
+ * @param path a path to an .apk-compatible archive that contains the license. An empty string
+ * in case the license is contained within the app itself.
+ */
+ static License create(String libraryName, long licenseOffset, int licenseLength, String path) {
+ return new License(libraryName, licenseOffset, licenseLength, path);
+ }
+
+ public static final Parcelable.Creator<License> CREATOR =
+ new Parcelable.Creator<License>() {
+ @Override
+ public License createFromParcel(Parcel in) {
+ return new License(in);
+ }
+
+ @Override
+ public License[] newArray(int size) {
+ return new License[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mLibraryName);
+ dest.writeLong(mLicenseOffset);
+ dest.writeInt(mLicenseLength);
+ dest.writeString(mPath);
+ }
+
+ @Override
+ public int compareTo(License o) {
+ return mLibraryName.compareToIgnoreCase(o.getLibraryName());
+ }
+
+ @Override
+ public String toString() {
+ return getLibraryName();
+ }
+
+ private License(String libraryName, long licenseOffset, int licenseLength, String path) {
+ this.mLibraryName = libraryName;
+ this.mLicenseOffset = licenseOffset;
+ this.mLicenseLength = licenseLength;
+ this.mPath = path;
+ }
+
+ private License(Parcel in) {
+ mLibraryName = in.readString();
+ mLicenseOffset = in.readLong();
+ mLicenseLength = in.readInt();
+ mPath = in.readString();
+ }
+
+ String getLibraryName() {
+ return mLibraryName;
+ }
+
+ long getLicenseOffset() {
+ return mLicenseOffset;
+ }
+
+ int getLicenseLength() {
+ return mLicenseLength;
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+}
diff --git a/src/com/android/tv/license/LicenseDialogFragment.java b/src/com/android/tv/license/LicenseDialogFragment.java
new file mode 100644
index 00000000..b0e09776
--- /dev/null
+++ b/src/com/android/tv/license/LicenseDialogFragment.java
@@ -0,0 +1,97 @@
+/*
+ * 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.license;
+
+import android.app.DialogFragment;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.text.method.ScrollingMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.dialog.SafeDismissDialogFragment;
+
+/** A DialogFragment that shows a License in a text view. */
+public class LicenseDialogFragment extends SafeDismissDialogFragment {
+ public static final String DIALOG_TAG = LicenseDialogFragment.class.getSimpleName();
+
+ private static final String LICENSE = "LICENSE";
+
+ private License mLicense;
+ private String mTrackerLabel;
+
+ /**
+ * Create a new LicenseDialogFragment to show a particular license.
+ *
+ * @param license The License to show.
+ */
+ public static LicenseDialogFragment newInstance(License license) {
+ LicenseDialogFragment f = new LicenseDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(LICENSE, license);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mLicense = getArguments().getParcelable(LICENSE);
+ String title = mLicense.getLibraryName();
+ mTrackerLabel = getArguments().getString(title + "_license");
+ int style =
+ TextUtils.isEmpty(title)
+ ? DialogFragment.STYLE_NO_TITLE
+ : DialogFragment.STYLE_NORMAL;
+ setStyle(style, 0);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ TextView textView = new TextView(getActivity());
+ String licenseText = Licenses.getLicenseText(getContext(), mLicense);
+ textView.setText(licenseText != null ? licenseText : "");
+ textView.setMovementMethod(new ScrollingMovementMethod());
+ int verticalOverscan =
+ getResources().getDimensionPixelSize(R.dimen.vertical_overscan_safe_margin);
+ int horizontalOverscan =
+ getResources().getDimensionPixelSize(R.dimen.horizontal_overscan_safe_margin);
+ textView.setPadding(
+ horizontalOverscan, verticalOverscan, horizontalOverscan, verticalOverscan);
+ return textView;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // Ensure the dialog is fullscreen, even if the TextView doesn't have its content yet.
+ getDialog().getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ getDialog().setTitle(mLicense.getLibraryName());
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return mTrackerLabel;
+ }
+}
diff --git a/src/com/android/tv/license/LicenseSideFragment.java b/src/com/android/tv/license/LicenseSideFragment.java
new file mode 100644
index 00000000..fd92467c
--- /dev/null
+++ b/src/com/android/tv/license/LicenseSideFragment.java
@@ -0,0 +1,80 @@
+/*
+ * 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.license;
+
+import android.content.Context;
+
+import com.android.tv.R;
+import com.android.tv.ui.sidepanel.ActionItem;
+import com.android.tv.ui.sidepanel.SideFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Opens a dialog showing open source licenses. */
+public final class LicenseSideFragment extends SideFragment {
+
+ public static final String TRACKER_LABEL = "Open Source Licenses";
+
+ public class LicenseActionItem extends ActionItem {
+ private final License license;
+
+ public LicenseActionItem(License license) {
+ super(license.getLibraryName());
+ this.license = license;
+ }
+
+ @Override
+ protected void onSelected() {
+ LicenseDialogFragment dialog = LicenseDialogFragment.newInstance(license);
+ getMainActivity()
+ .getOverlayManager()
+ .showDialogFragment(LicenseDialogFragment.DIALOG_TAG, dialog, true);
+ }
+ }
+
+ private List<LicenseActionItem> licenses;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ licenses = toActionItems(Licenses.getLicenses(context));
+ }
+
+ private List<LicenseActionItem> toActionItems(ArrayList<License> licenses) {
+ List<LicenseActionItem> items = new ArrayList<>(licenses.size());
+ for (License license : licenses) {
+ items.add(new LicenseActionItem(license));
+ }
+ return items;
+ }
+
+ @Override
+ protected String getTitle() {
+ return getResources().getString(R.string.settings_menu_licenses);
+ }
+
+ @Override
+ public String getTrackerLabel() {
+ return TRACKER_LABEL;
+ }
+
+ @Override
+ protected List<LicenseActionItem> getItemList() {
+ return licenses;
+ }
+}
diff --git a/src/com/android/tv/license/LicenseUtils.java b/src/com/android/tv/license/LicenseUtils.java
index b972aad6..cf3fe751 100644
--- a/src/com/android/tv/license/LicenseUtils.java
+++ b/src/com/android/tv/license/LicenseUtils.java
@@ -26,21 +26,9 @@ import java.io.InputStream;
* Utilities for showing open source licenses.
*/
public final class LicenseUtils {
- public final static String LICENSE_FILE = "file:///android_asset/licenses.html";
public final static String RATING_SOURCE_FILE =
"file:///android_asset/rating_sources.html";
- private final static File licenseFile = new File(LICENSE_FILE);
- /**
- * Checks if the license.html asset is include in the apk.
- */
- public static boolean hasLicenses(AssetManager am) {
- try (InputStream is = am.open("licenses.html")) {
- return true;
- } catch (IOException e) {
- return false;
- }
- }
/**
* Checks if the rating_attribution.html asset is include in the apk.
diff --git a/src/com/android/tv/license/Licenses.java b/src/com/android/tv/license/Licenses.java
new file mode 100644
index 00000000..4b8a7ffc
--- /dev/null
+++ b/src/com/android/tv/license/Licenses.java
@@ -0,0 +1,122 @@
+/*
+ * 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.license;
+
+import android.content.Context;
+import android.support.annotation.RawRes;
+
+import com.android.tv.R;
+import com.android.tv.common.SoftPreconditions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * A helper for extracting licenses embedded using
+ * third_party_licenses.build:third_party_licenses().
+ */
+public final class Licenses {
+
+ public static final String TAG = "Licenses";
+ public static boolean hasLicenses(Context context) {
+ return !getTextFromResource(
+ context.getApplicationContext(), R.raw.third_party_license_metadata, 0, -1)
+ .isEmpty();
+ }
+
+ /** Return the licenses bundled into this app. */
+ public static ArrayList<License> getLicenses(Context context) {
+ return getLicenseListFromMetadata(
+ getTextFromResource(
+ context.getApplicationContext(), R.raw.third_party_license_metadata, 0, -1),
+ "");
+ }
+
+ /**
+ * Returns a list of {@link License}s parsed from a license metadata file.
+ *
+ * @param metadata a {@code String} containing the contents of a license metadata file.
+ * @param filePath a path to a package archive with licenses or empty string for the app package
+ */
+ private static ArrayList<License> getLicenseListFromMetadata(String metadata, String filePath) {
+ String[] entries = metadata.split("\n");
+ ArrayList<License> licenses = new ArrayList<License>(entries.length);
+ for (String entry : entries) {
+ int delimiter = entry.indexOf(' ');
+ String[] licenseLocation = entry.substring(0, delimiter).split(":");
+ SoftPreconditions.checkState(
+ licenseLocation.length == 2 && delimiter > 0,
+ TAG,
+ "Invalid license meta-data line:\n" + entry);
+ long licenseOffset = Long.parseLong(licenseLocation[0]);
+ int licenseLength = Integer.parseInt(licenseLocation[1]);
+ licenses.add(
+ License.create(
+ entry.substring(delimiter + 1),
+ licenseOffset,
+ licenseLength,
+ filePath));
+ }
+ Collections.sort(licenses);
+ return licenses;
+ }
+
+ /** Return the text of a bundled license file. */
+ public static String getLicenseText(Context context, License license) {
+ long offset = license.getLicenseOffset();
+ int length = license.getLicenseLength();
+ return getTextFromResource(context, R.raw.third_party_licenses, offset, length);
+ }
+
+ private static String getTextFromResource(
+ Context context, @RawRes int resourcesIdentifier, long offset, int length) {
+ InputStream stream =
+ context.getApplicationContext().getResources().openRawResource(resourcesIdentifier);
+ return getTextFromInputStream(stream, offset, length);
+ }
+
+ private static String getTextFromInputStream(InputStream stream, long offset, int length) {
+ byte[] buffer = new byte[1024];
+ ByteArrayOutputStream textArray = new ByteArrayOutputStream();
+
+ try {
+ stream.skip(offset);
+ int bytesRemaining = length > 0 ? length : Integer.MAX_VALUE;
+ int bytes = 0;
+
+ while (bytesRemaining > 0
+ && (bytes = stream.read(buffer, 0, Math.min(bytesRemaining, buffer.length)))
+ != -1) {
+ textArray.write(buffer, 0, bytes);
+ bytesRemaining -= bytes;
+ }
+ stream.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to read license or metadata text.", e);
+ }
+ try {
+ return textArray.toString("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(
+ "Unsupported encoding UTF8. This should always be supported.", e);
+ }
+ }
+}
diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java
index 54892cac..2fd70bfb 100644
--- a/src/com/android/tv/menu/ActionCardView.java
+++ b/src/com/android/tv/menu/ActionCardView.java
@@ -19,8 +19,8 @@ package com.android.tv.menu;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
-import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import com.android.tv.R;
@@ -28,7 +28,7 @@ import com.android.tv.R;
/**
* A view to render an item of TV options.
*/
-public class ActionCardView extends FrameLayout implements ItemListRowView.CardView<MenuAction> {
+public class ActionCardView extends RelativeLayout implements ItemListRowView.CardView<MenuAction> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -66,7 +66,7 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV
}
mIconView.setImageDrawable(action.getDrawable(getContext()));
mLabelView.setText(action.getActionName(getContext()));
- mStateView.setText(action.getActionDescription(getContext()));
+ mStateView.setText(action.getActionDescription());
if (action.isEnabled()) {
setEnabled(true);
setFocusable(true);
diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java
index bfb5e3f1..94ccd37f 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;
@@ -40,10 +41,12 @@ import com.android.tv.util.BitmapUtils;
import com.android.tv.util.ImageLoader;
import com.android.tv.util.TvInputManagerHelper;
+import java.util.Objects;
+
/**
* A view to render an app link card.
*/
-public class AppLinkCardView extends BaseCardView<Channel> {
+public class AppLinkCardView extends BaseCardView<ChannelsRowItem> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -53,9 +56,9 @@ public class AppLinkCardView extends BaseCardView<Channel> {
private final int mIconHeight;
private final int mIconPadding;
private final int mIconColorFilter;
+ private final Drawable mDefaultDrawable;
private ImageView mImageView;
- private View mGradientView;
private TextView mAppInfoView;
private View mMetaViewHolder;
private Channel mChannel;
@@ -82,6 +85,7 @@ public class AppLinkCardView extends BaseCardView<Channel> {
mPackageManager = context.getPackageManager();
mTvInputManagerHelper = ((MainActivity) context).getTvInputManagerHelper();
mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null);
+ mDefaultDrawable = getResources().getDrawable(R.drawable.ic_recent_thumbnail_default, null);
}
/**
@@ -92,65 +96,153 @@ public class AppLinkCardView extends BaseCardView<Channel> {
}
@Override
- public void onBind(Channel channel, boolean selected) {
+ public void onBind(ChannelsRowItem item, boolean selected) {
+ Channel newChannel = item.getChannel();
+ boolean channelChanged = !Objects.equals(mChannel, newChannel);
+ String previousPosterArtUri = mChannel == null ? null : mChannel.getAppLinkPosterArtUri();
+ boolean posterArtChanged = previousPosterArtUri == null
+ || newChannel.getAppLinkPosterArtUri() == null
+ || !TextUtils.equals(previousPosterArtUri, newChannel.getAppLinkPosterArtUri());
+ mChannel = newChannel;
if (DEBUG) {
- Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected
+ Log.d(TAG, "onBind(channelName=" + mChannel.getDisplayName() + ", selected=" + selected
+ ")");
}
- mChannel = channel;
ApplicationInfo appInfo = mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId());
- int linkType = mChannel.getAppLinkType(getContext());
- mIntent = mChannel.getAppLinkIntent(getContext());
+ if (channelChanged) {
+ int linkType = mChannel.getAppLinkType(getContext());
+ mIntent = mChannel.getAppLinkIntent(getContext());
- 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));
- 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));
- } 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);
- }
- break;
- case Channel.APP_LINK_TYPE_APP:
- setText(getContext().getString(
- R.string.channels_item_app_link_app_launcher,
- mPackageManager.getApplicationLabel(appInfo)));
- mAppInfoView.setVisibility(GONE);
- mGradientView.setVisibility(GONE);
- break;
- default:
- mAppInfoView.setVisibility(GONE);
- mGradientView.setVisibility(GONE);
- Log.d(TAG, "Should not be here.");
- }
+ CharSequence appLabel;
+ switch (linkType) {
+ case Channel.APP_LINK_TYPE_CHANNEL:
+ setText(mChannel.getAppLinkText());
+ mAppInfoView.setVisibility(VISIBLE);
+ mAppInfoView.setCompoundDrawablePadding(mIconPadding);
+ mAppInfoView.setCompoundDrawablesRelative(null, null, null, null);
+ appLabel = mTvInputManagerHelper
+ .getTvInputApplicationLabel(mChannel.getInputId());
+ if (appLabel != null) {
+ mAppInfoView.setText(appLabel);
+ } else {
+ new AsyncTask<Void, Void, CharSequence>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
- if (mChannel.getAppLinkColor() == 0) {
- mMetaViewHolder.setBackgroundResource(R.color.channel_card_meta_background);
- } else {
- mMetaViewHolder.setBackgroundColor(mChannel.getAppLinkColor());
- }
+ @Override
+ protected CharSequence doInBackground(Void... params) {
+ if (appInfo != null) {
+ return mPackageManager.getApplicationLabel(appInfo);
+ }
+ return null;
+ }
- if (!TextUtils.isEmpty(mChannel.getAppLinkPosterArtUri())) {
- mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
- mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART,
- mCardImageWidth, mCardImageHeight, createChannelLogoCallback(this, mChannel,
- Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART));
- } else {
- setCardImageWithBanner(appInfo);
+ @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));
+ } else if (appInfo.icon != 0) {
+ Drawable appIcon = mTvInputManagerHelper
+ .getTvInputApplicationIcon(mChannel.getInputId());
+ if (appIcon != null) {
+ BitmapUtils.setColorFilterToDrawable(mIconColorFilter, appIcon);
+ appIcon.setBounds(0, 0, mIconWidth, mIconHeight);
+ mAppInfoView.setCompoundDrawablesRelative(appIcon, null, null, null);
+ } else {
+ new AsyncTask<Void, Void, Drawable>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return mPackageManager.getApplicationIcon(appInfo);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable appIcon) {
+ mTvInputManagerHelper.setTvInputApplicationIcon(
+ mLoadTvInputId, appIcon);
+ if (!mLoadTvInputId.equals(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:
+ appLabel = mTvInputManagerHelper
+ .getTvInputApplicationLabel(mChannel.getInputId());
+ if (appLabel != null) {
+ setText(getContext()
+ .getString(R.string.channels_item_app_link_app_launcher, appLabel));
+ } else {
+ new AsyncTask<Void, Void, CharSequence>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+
+ @Override
+ protected CharSequence doInBackground(Void... params) {
+ if (appInfo != null) {
+ return mPackageManager.getApplicationLabel(appInfo);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence appLabel) {
+ mTvInputManagerHelper.setTvInputApplicationLabel(
+ mLoadTvInputId, appLabel);
+ if (!mLoadTvInputId.equals(mChannel.getInputId())
+ || !isAttachedToWindow()) {
+ return;
+ }
+ setText(getContext()
+ .getString(
+ R.string.channels_item_app_link_app_launcher,
+ appLabel));
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ mAppInfoView.setVisibility(GONE);
+ break;
+ default:
+ mAppInfoView.setVisibility(GONE);
+ Log.d(TAG, "Should not be here.");
+ }
+
+ if (mChannel.getAppLinkColor() == 0) {
+ mMetaViewHolder.setBackgroundResource(R.color.channel_card_meta_background);
+ } else {
+ mMetaViewHolder.setBackgroundColor(mChannel.getAppLinkColor());
+ }
}
- // 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);
+ if (posterArtChanged) {
+ mImageView.setImageDrawable(mDefaultDrawable);
+ mImageView.setForeground(null);
+ if (!TextUtils.isEmpty(mChannel.getAppLinkPosterArtUri())) {
+ mChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART,
+ mCardImageWidth, mCardImageHeight, createChannelLogoCallback(this, mChannel,
+ Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART));
+ } else {
+ setCardImageWithBanner(appInfo);
+ }
+ }
+ super.onBind(item, selected);
}
private static ImageLoader.ImageLoaderCallback<AppLinkCardView> createChannelLogoCallback(
@@ -182,13 +274,14 @@ public class AppLinkCardView extends BaseCardView<Channel> {
}
}
BitmapUtils.setColorFilterToDrawable(mIconColorFilter, drawable);
- mAppInfoView.setCompoundDrawables(drawable, null, null, null);
+ mAppInfoView.setCompoundDrawablesRelative(drawable, null, null, null);
} else if (type == Channel.LOAD_IMAGE_TYPE_APP_LINK_POSTER_ART) {
if (bitmap == null) {
setCardImageWithBanner(
mTvInputManagerHelper.getTvInputAppInfo(mChannel.getInputId()));
} else {
mImageView.setImageBitmap(bitmap);
+ mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
if (mChannel.getAppLinkColor() == 0) {
extractAndSetMetaViewBackgroundColor(bitmap);
}
@@ -200,7 +293,6 @@ public class AppLinkCardView extends BaseCardView<Channel> {
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
- mGradientView = findViewById(R.id.image_gradient);
mAppInfoView = (TextView) findViewById(R.id.app_info);
mMetaViewHolder = findViewById(R.id.app_link_text_holder);
}
@@ -209,37 +301,85 @@ public class AppLinkCardView extends BaseCardView<Channel> {
// 1) Provided poster art image, 2) Activity banner, 3) Activity icon, 4) Application banner,
// 5) Application icon, and 6) default image.
private void setCardImageWithBanner(ApplicationInfo appInfo) {
- Drawable banner = null;
- if (mIntent != null) {
- try {
- banner = mPackageManager.getActivityBanner(mIntent);
- if (banner == null) {
- banner = mPackageManager.getActivityIcon(mIntent);
+ new AsyncTask<Void, Void, Drawable>() {
+ private String mLoadTvInputId = mChannel.getInputId();
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ Drawable banner = null;
+ if (mIntent != null) {
+ try {
+ banner = mPackageManager.getActivityBanner(mIntent);
+ if (banner == null) {
+ banner = mPackageManager.getActivityIcon(mIntent);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // do nothing.
+ }
}
- } catch (PackageManager.NameNotFoundException e) {
- // do nothing.
+ return banner;
}
- }
- if (banner == null && appInfo != null) {
- if (appInfo.banner != 0) {
- banner = mPackageManager.getApplicationBanner(appInfo);
- }
- if (banner == null && appInfo.icon != 0) {
- banner = mPackageManager.getApplicationIcon(appInfo);
+ @Override
+ protected void onPostExecute(Drawable banner) {
+ if (mLoadTvInputId != mChannel.getInputId() || !isAttachedToWindow()) {
+ return;
+ }
+ if (banner != null) {
+ setCardImageWithBannerInternal(banner);
+ } else {
+ setCardImageWithApplicationInfoBanner(appInfo);
+ }
}
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void setCardImageWithApplicationInfoBanner(ApplicationInfo appInfo) {
+ Drawable appBanner =
+ mTvInputManagerHelper.getTvInputApplicationBanner(mChannel.getInputId());
+ if (appBanner != null) {
+ setCardImageWithBannerInternal(appBanner);
+ } else {
+ new AsyncTask<Void, Void, Drawable>() {
+ private final String mLoadTvInputId = mChannel.getInputId();
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ Drawable banner = null;
+ if (appInfo != null) {
+ if (appInfo.banner != 0) {
+ banner = mPackageManager.getApplicationBanner(appInfo);
+ }
+ if (banner == null && appInfo.icon != 0) {
+ banner = mPackageManager.getApplicationIcon(appInfo);
+ }
+ }
+ return banner;
+ }
+
+ @Override
+ protected void onPostExecute(Drawable banner) {
+ mTvInputManagerHelper.setTvInputApplicationBanner(mLoadTvInputId, banner);
+ if (!TextUtils.equals(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.setImageDrawable(mDefaultDrawable);
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..4c5e6c78 100644
--- a/src/com/android/tv/menu/BaseCardView.java
+++ b/src/com/android/tv/menu/BaseCardView.java
@@ -22,6 +22,7 @@ import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Outline;
import android.support.annotation.Nullable;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
@@ -35,9 +36,6 @@ import com.android.tv.R;
* A base class to render a card.
*/
public abstract class BaseCardView<T> extends LinearLayout implements ItemListRowView.CardView<T> {
- private static final String TAG = "BaseCardView";
- private static final boolean DEBUG = false;
-
private static final float SCALE_FACTOR_0F = 0f;
private static final float SCALE_FACTOR_1F = 1f;
@@ -57,6 +55,11 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
private TextView mTextViewFocused;
private final int mCardImageWidth;
private final float mCardHeight;
+ private boolean mSelected;
+
+ private int mTextResId;
+ private String mTextString;
+ private boolean mTextChanged;
public BaseCardView(Context context) {
this(context, null);
@@ -103,23 +106,9 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
/**
* Called when the view is displayed.
- *
- * Before onBind is called, this view's text should be set to determine if it'll be extended
- * or not in focus state.
*/
@Override
public void onBind(T item, boolean selected) {
- if (mTextView != null && mTextViewFocused != null) {
- mTextViewFocused.measure(
- MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
- mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1;
- if (mExtendViewOnFocus) {
- setTextViewFocusedAlpha(selected ? 1f : 0f);
- } else {
- setTextViewFocusedAlpha(1f);
- }
- }
setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F);
}
@@ -128,6 +117,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
@Override
public void onSelected() {
+ mSelected = true;
if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
startFocusAnimation(SCALE_FACTOR_1F);
} else {
@@ -138,6 +128,7 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
@Override
public void onDeselected() {
+ mSelected = false;
if (isAttachedToWindow() && getVisibility() == View.VISIBLE) {
startFocusAnimation(SCALE_FACTOR_0F);
} else {
@@ -150,11 +141,17 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
* Sets text of this card view.
*/
public void setText(int resId) {
- if (mTextViewFocused != null) {
- mTextViewFocused.setText(resId);
- }
- if (mTextView != null) {
- mTextView.setText(resId);
+ if (mTextResId != resId) {
+ mTextResId = resId;
+ mTextString = null;
+ mTextChanged = true;
+ if (mTextViewFocused != null) {
+ mTextViewFocused.setText(resId);
+ }
+ if (mTextView != null) {
+ mTextView.setText(resId);
+ }
+ onTextViewUpdated();
}
}
@@ -162,12 +159,33 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
* Sets text of this card view.
*/
public void setText(String text) {
- if (mTextViewFocused != null) {
- mTextViewFocused.setText(text);
+ if (!TextUtils.equals(text, mTextString)) {
+ mTextString = text;
+ mTextResId = 0;
+ mTextChanged = true;
+ if (mTextViewFocused != null) {
+ mTextViewFocused.setText(text);
+ }
+ if (mTextView != null) {
+ mTextView.setText(text);
+ }
+ onTextViewUpdated();
}
- if (mTextView != null) {
- mTextView.setText(text);
+ }
+
+ 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 +227,18 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo
setScaleX(scale);
setScaleY(scale);
setTranslationZ(mFocusTranslationZ * animatedValue);
- if (mExtendViewOnFocus) {
+ if (mTextView != null && mTextViewFocused != null) {
ViewGroup.LayoutParams params = mTextView.getLayoutParams();
- params.height = Math.round(mTextViewHeight
- + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue);
- setTextViewLayoutParams(params);
- setTextViewFocusedAlpha(animatedValue);
+ int height = mExtendViewOnFocus ? Math.round(mTextViewHeight
+ + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue)
+ : (int) mTextViewHeight;
+ if (height != params.height) {
+ params.height = height;
+ setTextViewLayoutParams(params);
+ }
+ if (mExtendViewOnFocus) {
+ setTextViewFocusedAlpha(animatedValue);
+ }
}
}
diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java
index 1c8015a6..2ecb6af7 100644
--- a/src/com/android/tv/menu/ChannelCardView.java
+++ b/src/com/android/tv/menu/ChannelCardView.java
@@ -34,10 +34,12 @@ import com.android.tv.data.Program;
import com.android.tv.parental.ParentalControlSettings;
import com.android.tv.util.ImageLoader;
+import java.util.Objects;
+
/**
* A view to render channel card.
*/
-public class ChannelCardView extends BaseCardView<Channel> {
+public class ChannelCardView extends BaseCardView<ChannelsRowItem> {
private static final String TAG = MenuView.TAG;
private static final boolean DEBUG = MenuView.DEBUG;
@@ -45,11 +47,11 @@ public class ChannelCardView extends BaseCardView<Channel> {
private final int mCardImageHeight;
private ImageView mImageView;
- private View mGradientView;
private TextView mChannelNumberNameView;
private ProgressBar mProgressBar;
private Channel mChannel;
private Program mProgram;
+ private String mPosterArtUri;
private final MainActivity mMainActivity;
public ChannelCardView(Context context) {
@@ -71,39 +73,72 @@ public class ChannelCardView extends BaseCardView<Channel> {
protected void onFinishInflate() {
super.onFinishInflate();
mImageView = (ImageView) findViewById(R.id.image);
- mGradientView = findViewById(R.id.image_gradient);
+ mImageView.setBackgroundResource(R.color.channel_card);
mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name);
mProgressBar = (ProgressBar) findViewById(R.id.progress);
}
@Override
- public void onBind(Channel channel, boolean selected) {
+ public void onBind(ChannelsRowItem item, boolean selected) {
if (DEBUG) {
- Log.d(TAG, "onBind(channelName=" + channel.getDisplayName() + ", selected=" + selected
- + ")");
+ Log.d(TAG, "onBind(channelName=" + item.getChannel().getDisplayName() + ", selected="
+ + selected + ")");
}
- mChannel = channel;
- mProgram = null;
- mChannelNumberNameView.setText(mChannel.getDisplayText());
- mChannelNumberNameView.setVisibility(VISIBLE);
- mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
- mImageView.setBackgroundResource(R.color.channel_card);
- mGradientView.setVisibility(View.GONE);
- mProgressBar.setVisibility(GONE);
+ updateChannel(item);
+ updateProgram();
+ super.onBind(item, selected);
+ }
- setTextViewEnabled(true);
- if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled()
- && mChannel.isLocked()) {
+ private void updateChannel(ChannelsRowItem item) {
+ if (!item.getChannel().equals(mChannel)) {
+ mChannel = item.getChannel();
+ mChannelNumberNameView.setText(mChannel.getDisplayText());
+ mChannelNumberNameView.setVisibility(VISIBLE);
+ }
+ }
+
+ private void updateProgram() {
+ ParentalControlSettings parental = mMainActivity.getParentalControlSettings();
+ if (parental.isParentalControlsEnabled() && mChannel.isLocked()) {
setText(R.string.program_title_for_blocked_channel);
- return;
+ mProgram = null;
+ } else {
+ Program currentProgram =
+ mMainActivity.getProgramDataManager().getCurrentProgram(mChannel.getId());
+ if (!Objects.equals(currentProgram, mProgram)) {
+ mProgram = currentProgram;
+ if (mProgram == null || TextUtils.isEmpty(mProgram.getTitle())) {
+ setTextViewEnabled(false);
+ setText(R.string.program_title_for_no_information);
+ } else {
+ setTextViewEnabled(true);
+ setText(mProgram.getTitle());
+ }
+ }
+ }
+ if (mProgram == null) {
+ mProgressBar.setVisibility(GONE);
+ setPosterArt(null);
} else {
- setText("");
+ // Update progress.
+ mProgressBar.setVisibility(View.VISIBLE);
+ long startTime = mProgram.getStartTimeUtcMillis();
+ long endTime = mProgram.getEndTimeUtcMillis();
+ long currTime = System.currentTimeMillis();
+ if (currTime <= startTime) {
+ mProgressBar.setProgress(0);
+ } else if (currTime >= endTime) {
+ mProgressBar.setProgress(100);
+ } else {
+ mProgressBar.setProgress(
+ (int) (100 * (currTime - startTime) / (endTime - startTime)));
+ }
+ // Update image.
+ if (!parental.isParentalControlsEnabled()
+ || !parental.isRatingBlocked(mProgram.getContentRatings())) {
+ setPosterArt(mProgram.getPosterArtUri());
+ }
}
-
- 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);
}
private static ImageLoader.ImageLoaderCallback<ChannelCardView> createProgramPosterArtCallback(
@@ -121,49 +156,20 @@ public class ChannelCardView extends BaseCardView<Channel> {
};
}
- private void updatePosterArt(Bitmap posterArt) {
- mImageView.setImageBitmap(posterArt);
- mGradientView.setVisibility(View.VISIBLE);
- }
-
- private void updateProgramInformation() {
- if (mChannel == null) {
- return;
- }
- mProgram = mMainActivity.getProgramDataManager().getCurrentProgram(mChannel.getId());
- if (mProgram == null || TextUtils.isEmpty(mProgram.getTitle())) {
- setTextViewEnabled(false);
- setText(R.string.program_title_for_no_information);
- } else {
- setText(mProgram.getTitle());
- }
-
- if (mProgram == null) {
- return;
- }
-
- long startTime = mProgram.getStartTimeUtcMillis();
- long endTime = mProgram.getEndTimeUtcMillis();
- long currTime = System.currentTimeMillis();
- mProgressBar.setVisibility(View.VISIBLE);
- if (currTime <= startTime) {
- mProgressBar.setProgress(0);
- } else if (currTime >= endTime) {
- mProgressBar.setProgress(100);
- } else {
- mProgressBar.setProgress((int) (100 * (currTime - startTime) / (endTime - startTime)));
+ private void setPosterArt(String posterArtUri) {
+ if (!TextUtils.equals(mPosterArtUri, posterArtUri)) {
+ mPosterArtUri = posterArtUri;
+ if (posterArtUri == null
+ || !mProgram.loadPosterArt(getContext(), mCardImageWidth, mCardImageHeight,
+ createProgramPosterArtCallback(this, mProgram))) {
+ mImageView.setImageResource(R.drawable.ic_recent_thumbnail_default);
+ mImageView.setForeground(null);
+ }
}
+ }
- if (!(getContext() instanceof MainActivity)) {
- Log.e(TAG, "Fails to check program's content rating.");
- return;
- }
- ParentalControlSettings parental = mMainActivity.getParentalControlSettings();
- if ((!parental.isParentalControlsEnabled()
- || !parental.isRatingBlocked(mProgram.getContentRatings()))
- && !TextUtils.isEmpty(mProgram.getPosterArtUri())) {
- mProgram.loadPosterArt(getContext(), mCardImageWidth, mCardImageHeight,
- createProgramPosterArtCallback(this, mProgram));
- }
+ private void updatePosterArt(Bitmap posterArt) {
+ mImageView.setImageBitmap(posterArt);
+ mImageView.setForeground(getContext().getDrawable(R.drawable.card_image_gradient));
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
index f932d75d..bc5d6cfb 100644
--- a/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
+++ b/src/com/android/tv/menu/ChannelsPosterPrefetcher.java
@@ -97,12 +97,13 @@ public class ChannelsPosterPrefetcher {
// This executes on the main thread, but since the item list is expected to be about 5 items
// and ImageLoader spawns an async task so this is fast enough. 1 ms in local testing.
- List<Channel> channelList = mChannelsAdapter.getItemList();
- if (channelList != null) {
- for (Channel channel : channelList) {
+ List<ChannelsRowItem> items = mChannelsAdapter.getItemList();
+ if (items != null) {
+ for (ChannelsRowItem item : items) {
if (isCanceled) {
return;
}
+ Channel channel = item.getChannel();
if (!Channel.isValid(channel)) {
continue;
}
@@ -116,7 +117,7 @@ public class ChannelsPosterPrefetcher {
}
if (DEBUG) {
Log.d(TAG, "doPrefetchImages() finished. ImageLoader may still have async tasks for "
- + "channels " + channelList);
+ + "channels " + items);
}
}
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..7ff44ea6 100644
--- a/src/com/android/tv/menu/ChannelsRowAdapter.java
+++ b/src/com/android/tv/menu/ChannelsRowAdapter.java
@@ -31,17 +31,15 @@ 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.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
/**
* An adapter of the Channels row.
*/
-public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> {
- private static final String TAG = "ChannelsRowAdapter";
-
+public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<ChannelsRowItem> {
// There are four special cards: guide, setup, dvr, applink.
private static final int SIZE_OF_VIEW_TYPE = 5;
@@ -51,7 +49,6 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
private final DvrDataManager mDvrDataManager;
private final int mMaxCount;
private final int mMinCount;
- private final int[] mViewType = new int[SIZE_OF_VIEW_TYPE];
private final View.OnClickListener mGuideOnClickListener = new View.OnClickListener() {
@Override
@@ -113,14 +110,12 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
mRecommender = recommender;
mMinCount = minCount;
mMaxCount = maxCount;
+ setHasStableIds(true);
}
@Override
public int getItemViewType(int position) {
- if (position >= SIZE_OF_VIEW_TYPE) {
- return R.layout.menu_card_channel;
- }
- return mViewType[position];
+ return getItemList().get(position).getLayoutId();
}
@Override
@@ -129,9 +124,12 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
}
@Override
- public void onBindViewHolder(MyViewHolder viewHolder, int position) {
- super.onBindViewHolder(viewHolder, position);
+ public long getItemId(int position) {
+ return getItemList().get(position).getItemId();
+ }
+ @Override
+ public void onBindViewHolder(MyViewHolder viewHolder, int position) {
int viewType = getItemViewType(position);
if (viewType == R.layout.menu_card_guide) {
viewHolder.itemView.setOnClickListener(mGuideOnClickListener);
@@ -144,80 +142,158 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel>
SimpleCardView view = (SimpleCardView) viewHolder.itemView;
view.setText(R.string.channels_item_dvr);
} else {
- viewHolder.itemView.setTag(getItemList().get(position));
+ viewHolder.itemView.setTag(getItemList().get(position).getChannel());
viewHolder.itemView.setOnClickListener(mChannelOnClickListener);
}
+ super.onBindViewHolder(viewHolder, position);
}
@Override
public void update() {
- List<Channel> channelList = new ArrayList<>();
- Channel dummyChannel = new Channel.Builder().build();
- // For guide item
- channelList.add(dummyChannel);
- // For setup item
- TvInputManagerHelper inputManager = TvApplication.getSingletons(mContext)
- .getTvInputManagerHelper();
- boolean showSetupCard = SetupUtils.getInstance(mContext).hasNewInput(inputManager);
- Channel currentChannel = getMainActivity().getCurrentChannel();
- boolean showAppLinkCard = currentChannel != null
- && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE
- // Sometimes applicationInfo can be null. b/28932537
- && inputManager.getTvInputAppInfo(currentChannel.getInputId()) != null;
- boolean showDvrCard = false;
+ if (getItemCount() == 0) {
+ createItems();
+ } else {
+ updateItems();
+ }
+ }
+
+ private void createItems() {
+ List<ChannelsRowItem> items = new ArrayList<>();
+ items.add(ChannelsRowItem.GUIDE_ITEM);
+ if (needToShowSetupItem()) {
+ items.add(ChannelsRowItem.SETUP_ITEM);
+ }
+ if (needToShowDvrItem()) {
+ items.add(ChannelsRowItem.DVR_ITEM);
+ }
+ if (needToShowAppLinkItem()) {
+ ChannelsRowItem.APP_LINK_ITEM.setChannel(
+ new Channel.Builder(getMainActivity().getCurrentChannel()).build());
+ items.add(ChannelsRowItem.APP_LINK_ITEM);
+ }
+ for (Channel channel : getRecentChannels()) {
+ items.add(new ChannelsRowItem(channel, R.layout.menu_card_channel));
+ }
+ setItemList(items);
+ }
+
+ private void updateItems() {
+ List<ChannelsRowItem> items = getItemList();
+ // The current index of the item list to iterate. It starts from 1 because the first item
+ // (GUIDE) is always visible and not updated.
+ int currentIndex = 1;
+ if (updateItem(needToShowSetupItem(), ChannelsRowItem.SETUP_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
+ if (updateItem(needToShowDvrItem(), ChannelsRowItem.DVR_ITEM, currentIndex)) {
+ ++currentIndex;
+ }
+ if (updateItem(needToShowAppLinkItem(), ChannelsRowItem.APP_LINK_ITEM, currentIndex)) {
+ if (!getMainActivity().getCurrentChannel()
+ .hasSameReadOnlyInfo(ChannelsRowItem.APP_LINK_ITEM.getChannel())) {
+ ChannelsRowItem.APP_LINK_ITEM.setChannel(
+ new Channel.Builder(getMainActivity().getCurrentChannel()).build());
+ notifyItemChanged(currentIndex);
+ }
+ ++currentIndex;
+ }
+ int numOldChannels = items.size() - currentIndex;
+ if (numOldChannels > 0) {
+ while (items.size() > currentIndex) {
+ items.remove(items.size() - 1);
+ }
+ notifyItemRangeRemoved(currentIndex, numOldChannels);
+ }
+ for (Channel channel : getRecentChannels()) {
+ items.add(new ChannelsRowItem(channel, R.layout.menu_card_channel));
+ }
+ int numNewChannels = items.size() - currentIndex;
+ if (numNewChannels > 0) {
+ notifyItemRangeInserted(currentIndex, numNewChannels);
+ }
+ }
+
+ /**
+ * Returns {@code true} if the item should be shown.
+ */
+ private boolean updateItem(boolean needToShow, ChannelsRowItem item, int index) {
+ List<ChannelsRowItem> items = getItemList();
+ boolean isItemInList = index < items.size() && item.equals(items.get(index));
+ if (needToShow && !isItemInList) {
+ items.add(index, item);
+ notifyItemInserted(index);
+ } else if (!needToShow && isItemInList) {
+ items.remove(index);
+ notifyItemRemoved(index);
+ }
+ return needToShow;
+ }
+
+ private boolean needToShowSetupItem() {
+ TvInputManagerHelper inputManager =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ return SetupUtils.getInstance(mContext).hasNewInput(inputManager);
+ }
+
+ private boolean needToShowDvrItem() {
+ TvInputManagerHelper inputManager =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
if (mDvrDataManager != null) {
for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) {
if (info.canRecord()) {
- showDvrCard = true;
- break;
+ return true;
}
}
}
+ return false;
+ }
- mViewType[0] = R.layout.menu_card_guide;
- int index = 1;
- if (showSetupCard) {
- channelList.add(dummyChannel);
- mViewType[index++] = R.layout.menu_card_setup;
- }
- if (showDvrCard) {
- channelList.add(dummyChannel);
- mViewType[index++] = R.layout.menu_card_dvr;
- }
- if (showAppLinkCard) {
- channelList.add(currentChannel);
- mViewType[index++] = R.layout.menu_card_app_link;
- }
- for ( ; index < mViewType.length; ++index) {
- mViewType[index] = R.layout.menu_card_channel;
- }
- channelList.addAll(getRecentChannels());
- setItemList(channelList);
+ private boolean needToShowAppLinkItem() {
+ TvInputManagerHelper inputManager =
+ TvApplication.getSingletons(mContext).getTvInputManagerHelper();
+ Channel currentChannel = getMainActivity().getCurrentChannel();
+ return currentChannel != null
+ && currentChannel.getAppLinkType(mContext) != Channel.APP_LINK_TYPE_NONE
+ // Sometimes applicationInfo can be null. b/28932537
+ && inputManager.getTvInputAppInfo(currentChannel.getInputId()) != null;
}
private List<Channel> getRecentChannels() {
List<Channel> channelList = new ArrayList<>();
+ long currentChannelId = getMainActivity().getCurrentChannelId();
+ ArrayDeque<Long> recentChannels = getMainActivity().getRecentChannels();
+ // Add the last watched channel as the first one.
+ for (long channelId : recentChannels) {
+ if (addChannelToList(
+ channelList, mRecommender.getChannel(channelId), currentChannelId)) {
+ break;
+ }
+ }
+ // Add the recommended channels.
for (Channel channel : mRecommender.recommendChannels(mMaxCount)) {
- if (channel.isBrowsable()) {
- channelList.add(channel);
+ if (channelList.size() >= mMaxCount) {
+ break;
}
+ addChannelToList(channelList, channel, currentChannelId);
}
- int count = channelList.size();
// If the number of recommended channels is not enough, add more from the recent channel
// list.
- if (count < mMinCount) {
- for (long channelId : getMainActivity().getRecentChannels()) {
- Channel channel = mRecommender.getChannel(channelId);
- if (channel == null || channelList.contains(channel)
- || !channel.isBrowsable()) {
- continue;
- }
- channelList.add(channel);
- if (++count >= mMinCount) {
- break;
- }
+ for (long channelId : recentChannels) {
+ if (channelList.size() >= mMinCount) {
+ break;
}
+ addChannelToList(channelList, mRecommender.getChannel(channelId), currentChannelId);
}
return channelList;
}
+
+ private static boolean addChannelToList(
+ List<Channel> channelList, Channel channel, long currentChannelId) {
+ if (channel == null || channel.getId() == currentChannelId
+ || channelList.contains(channel) || !channel.isBrowsable()) {
+ return false;
+ }
+ channelList.add(channel);
+ return true;
+ }
}
diff --git a/src/com/android/tv/menu/ChannelsRowItem.java b/src/com/android/tv/menu/ChannelsRowItem.java
new file mode 100644
index 00000000..c35189ec
--- /dev/null
+++ b/src/com/android/tv/menu/ChannelsRowItem.java
@@ -0,0 +1,101 @@
+/*
+ * 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.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.android.tv.R;
+import com.android.tv.data.Channel;
+
+/**
+ * A class for the items in channels row.
+ */
+public class ChannelsRowItem {
+ /** The item ID for guide item */
+ public static final int GUIDE_ITEM_ID = -1;
+ /** The item ID for setup item */
+ public static final int SETUP_ITEM_ID = -2;
+ /** The item ID for DVR item */
+ public static final int DVR_ITEM_ID = -3;
+ /** The item ID for app link item */
+ public static final int APP_LINK_ITEM_ID = -4;
+
+ /** The item which represents the guide. */
+ public static final ChannelsRowItem GUIDE_ITEM =
+ new ChannelsRowItem(GUIDE_ITEM_ID, R.layout.menu_card_guide);
+ /** The item which represents the setup. */
+ public static final ChannelsRowItem SETUP_ITEM =
+ new ChannelsRowItem(SETUP_ITEM_ID, R.layout.menu_card_setup);
+ /** The item which represents the DVR. */
+ public static final ChannelsRowItem DVR_ITEM =
+ new ChannelsRowItem(DVR_ITEM_ID, R.layout.menu_card_dvr);
+ /** The item which represents the app link. */
+ public static final ChannelsRowItem APP_LINK_ITEM =
+ new ChannelsRowItem(APP_LINK_ITEM_ID, R.layout.menu_card_app_link);
+
+ private final long mItemId;
+ @NonNull private Channel mChannel;
+ private final int mLayoutId;
+
+ public ChannelsRowItem(@NonNull Channel channel, int layoutId) {
+ this(channel.getId(), layoutId);
+ mChannel = channel;
+ }
+
+ private ChannelsRowItem(long itemId, int layoutId) {
+ mItemId = itemId;
+ mLayoutId = layoutId;
+ }
+
+ /**
+ * Returns the channel for this item.
+ */
+ @NonNull
+ public Channel getChannel() {
+ return mChannel;
+ }
+
+ /**
+ * Sets the channel.
+ */
+ public void setChannel(@NonNull Channel channel) {
+ mChannel = channel;
+ }
+
+ /**
+ * Returns the layout resource ID to represent this item.
+ */
+ public int getLayoutId() {
+ return mLayoutId;
+ }
+
+ /**
+ * Returns the unique ID for this item.
+ */
+ public long getItemId() {
+ return mItemId;
+ }
+
+ @Override
+ public String toString() {
+ return "ChannelsRowItem{"
+ + "itemId=" + mItemId
+ + ", layoutId=" + mLayoutId
+ + ", channel=" + mChannel + "}";
+ }
+}
diff --git a/src/com/android/tv/menu/ItemListRowView.java b/src/com/android/tv/menu/ItemListRowView.java
index 4919c595..cbeee936 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;
@@ -69,6 +70,8 @@ public class ItemListRowView extends MenuRowView implements OnChildSelectedListe
protected void onFinishInflate() {
super.onFinishInflate();
mListView = (HorizontalGridView) getContentsView();
+ // Disable the position change animation of the cards.
+ mListView.setItemAnimator(null);
}
@Override
@@ -194,9 +197,24 @@ 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);
+ }
+
+ /**
+ * Returns {@code true} if the item list contains the item, otherwise {@code false}.
+ */
+ protected boolean containsItem(T item) {
+ return mItemList.contains(item);
+ }
+
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View view = mLayoutInflater.inflate(getLayoutResId(viewType), parent, false);
+ View view = ViewCache.getInstance().getOrCreateView(
+ mLayoutInflater, getLayoutResId(viewType), parent);
return new MyViewHolder(view);
}
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 1160a5b5..e373de61 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -26,24 +26,28 @@ import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
+import android.support.v17.leanback.widget.HorizontalGridView;
import android.util.Log;
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 +85,21 @@ public class Menu {
sRowIdListForReason.add(PlayControlsRow.ID); // REASON_PLAY_CONTROLS_JUMP_TO_NEXT
}
+ private static final Map<Integer, Integer> PRELOAD_VIEW_IDS = new HashMap<>();
+ static {
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_guide, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_setup, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_dvr, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_app_link, 1);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_channel, ChannelsRow.MAX_COUNT_FOR_RECENT_CHANNELS);
+ PRELOAD_VIEW_IDS.put(R.layout.menu_card_action, 7);
+ }
+
private static final String SCREEN_NAME = "Menu";
private static final int MSG_HIDE_MENU = 1000;
+ private final Context mContext;
private final IMenuView mMenuView;
private final Tracker mTracker;
private final DurationTimer mVisibleTimer = new DurationTimer();
@@ -103,15 +118,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 +146,6 @@ public class Menu {
addMenuRow(menuRowFactory.createMenuRow(this, ChannelsRow.class));
addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class));
addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class));
- addMenuRow(menuRowFactory.createMenuRow(this, PipOptionsRow.class));
mMenuView.setMenuRows(mMenuRows);
}
@@ -160,6 +175,16 @@ public class Menu {
}
/**
+ * Preloads the item view used for the menu.
+ */
+ public void preloadItemViews() {
+ HorizontalGridView fakeParent = new HorizontalGridView(mContext);
+ for (int id : PRELOAD_VIEW_IDS.keySet()) {
+ ViewCache.getInstance().putView(mContext, id, fakeParent, PRELOAD_VIEW_IDS.get(id));
+ }
+ }
+
+ /**
* Shows the main menu.
*
* @param reason A reason why this is called. See {@link MenuShowReason}
@@ -293,9 +318,7 @@ public class Menu {
*/
public void onStreamInfoChanged() {
if (DEBUG) Log.d(TAG, "update options row in main menu");
- for (MenuRow row : mMenuRows) {
- row.onStreamInfoChanged();
- }
+ mMenuUpdater.onStreamInfoChanged();
}
@VisibleForTesting
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..173d4004 100644
--- a/src/com/android/tv/menu/MenuLayoutManager.java
+++ b/src/com/android/tv/menu/MenuLayoutManager.java
@@ -28,6 +28,7 @@ import android.support.annotation.UiThread;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.Property;
import android.view.View;
@@ -56,12 +57,14 @@ public class MenuLayoutManager {
// The visible duration of the title before it is hidden.
private static final long TITLE_SHOW_DURATION_BEFORE_HIDDEN_MS = TimeUnit.SECONDS.toMillis(2);
+ private static final int INVALID_POSITION = -1;
private final MenuView mMenuView;
private final List<MenuRow> mMenuRows = new ArrayList<>();
private final List<MenuRowView> mMenuRowViews = new ArrayList<>();
private final List<Integer> mRemovingRowViews = new ArrayList<>();
- private int mSelectedPosition = -1;
+ private int mSelectedPosition = INVALID_POSITION;
+ private int mPendingSelectedPosition = INVALID_POSITION;
private final int mRowAlignFromBottom;
private final int mRowContentsPaddingTop;
@@ -130,8 +133,8 @@ public class MenuLayoutManager {
MenuRowView currentView = mMenuRowViews.get(mSelectedPosition);
if (currentView.getVisibility() == View.GONE) {
// If the selected row is not visible, select the first visible row.
- int firstVisiblePosition = findNextVisiblePosition(-1);
- if (firstVisiblePosition != -1) {
+ int firstVisiblePosition = findNextVisiblePosition(INVALID_POSITION);
+ if (firstVisiblePosition != INVALID_POSITION) {
mSelectedPosition = firstVisiblePosition;
} else {
// No rows are visible.
@@ -157,6 +160,10 @@ public class MenuLayoutManager {
view.onDeselected();
}
}
+
+ if (mPendingSelectedPosition != INVALID_POSITION) {
+ setSelectedPositionSmooth(mPendingSelectedPosition);
+ }
}
private int findNextVisiblePosition(int start) {
@@ -166,7 +173,7 @@ public class MenuLayoutManager {
return i;
}
}
- return -1;
+ return INVALID_POSITION;
}
private void dumpChildren(String prefix) {
@@ -327,6 +334,7 @@ public class MenuLayoutManager {
mMenuRowViews.get(mSelectedPosition).onDeselected();
}
mSelectedPosition = position;
+ mPendingSelectedPosition = INVALID_POSITION;
if (Utils.isIndexValid(mMenuRowViews, mSelectedPosition)) {
mMenuRowViews.get(mSelectedPosition).onSelected(false);
}
@@ -380,14 +388,29 @@ public class MenuLayoutManager {
// again from the intermediate state.
mTitleFadeOutAnimator.cancel();
}
- final int oldPosition = mSelectedPosition;
- 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();
+ }
+ // When contents view's visibility is gone, layouting might be delayed until it's shown and
+ // thus cause onBindViewHolder() and menu action updating occurs in front of users' sight.
+ // Therefore we call requestLayout() here if there are pending adapter updates.
+ if (currentContentsView instanceof RecyclerView
+ && ((RecyclerView) currentContentsView).hasPendingAdapterUpdates()) {
+ currentContentsView.requestLayout();
+ mPendingSelectedPosition = position;
+ return;
+ }
+ final int oldPosition = mSelectedPosition;
+ mSelectedPosition = position;
+ mPendingSelectedPosition = INVALID_POSITION;
// Request focus after the new contents view shows up.
mMenuView.requestFocus();
if (mTempTitleViewForOld == null) {
@@ -407,7 +430,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 +491,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.
@@ -529,7 +550,7 @@ public class MenuLayoutManager {
int nextPosition;
if (scrollDown) {
nextPosition = findNextVisiblePosition(position);
- if (nextPosition != -1) {
+ if (nextPosition != INVALID_POSITION) {
MenuRowView nextView = mMenuRowViews.get(nextPosition);
Rect nextLayoutRect = layouts.get(nextPosition);
animators.add(createTranslationYAnimator(nextView,
@@ -539,7 +560,7 @@ public class MenuLayoutManager {
}
} else {
nextPosition = findNextVisiblePosition(oldPosition);
- if (nextPosition != -1) {
+ if (nextPosition != INVALID_POSITION) {
MenuRowView nextView = mMenuRowViews.get(nextPosition);
animators.add(createTranslationYAnimator(nextView, 0, mRowScrollUpAnimationOffset));
animators.add(createAlphaAnimator(nextView,
@@ -572,9 +593,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/MenuRow.java b/src/com/android/tv/menu/MenuRow.java
index 6f98e615..47804f11 100644
--- a/src/com/android/tv/menu/MenuRow.java
+++ b/src/com/android/tv/menu/MenuRow.java
@@ -123,11 +123,6 @@ public abstract class MenuRow {
public void onRecentChannelsChanged() { }
/**
- * This method is called when stream information is changed.
- */
- public void onStreamInfoChanged() { }
-
- /**
* Returns whether to hide the title when the row is selected.
*/
public boolean hideTitleWhenSelected() {
diff --git a/src/com/android/tv/menu/MenuRowFactory.java b/src/com/android/tv/menu/MenuRowFactory.java
index c67a0e04..570cfb8f 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,36 +75,15 @@ public class MenuRowFactory {
* A menu row which represents the TV options row.
*/
public static class TvOptionsRow extends ItemListRow {
+ /**
+ * The ID of the row.
+ */
+ public static final String ID = TvOptionsRow.class.getName();
+
private TvOptionsRow(Context context, Menu menu, List<CustomAction> customActions) {
super(context, menu, R.string.menu_title_options, R.dimen.action_card_height,
new TvOptionsRowAdapter(context, customActions));
}
-
- @Override
- public void onStreamInfoChanged() {
- if (getMenu().isActive()) {
- update();
- }
- }
- }
-
- /**
- * 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();
- }
}
/**
diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java
index 075b299e..18416c85 100644
--- a/src/com/android/tv/menu/MenuUpdater.java
+++ b/src/com/android/tv/menu/MenuUpdater.java
@@ -16,11 +16,14 @@
package com.android.tv.menu;
-import android.content.Context;
import android.support.annotation.Nullable;
import com.android.tv.ChannelTuner;
+import com.android.tv.TvOptionsManager;
+import com.android.tv.TvOptionsManager.OptionChangedListener;
+import com.android.tv.TvOptionsManager.OptionType;
import com.android.tv.data.Channel;
+import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
import com.android.tv.ui.TunableTvView;
import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener;
@@ -30,10 +33,10 @@ import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener;
* <p>As the menu is updated when it shows up, this class handles only the dynamic updates.
*/
public class MenuUpdater {
- // Can be null for testing.
- @Nullable
- private final TunableTvView mTvView;
private final Menu mMenu;
+ // Can be null for testing.
+ @Nullable private final TunableTvView mTvView;
+ @Nullable private final TvOptionsManager mOptionsManager;
private ChannelTuner mChannelTuner;
private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
@@ -42,7 +45,7 @@ public class MenuUpdater {
@Override
public void onBrowsableChannelListChanged() {
- mMenu.update();
+ mMenu.update(ChannelsRow.ID);
}
@Override
@@ -53,10 +56,17 @@ public class MenuUpdater {
mMenu.update(ChannelsRow.ID);
}
};
+ private final OptionChangedListener mOptionChangeListener = new OptionChangedListener() {
+ @Override
+ public void onOptionChanged(@OptionType int optionType, String newString) {
+ mMenu.update(TvOptionsRow.ID);
+ }
+ };
- public MenuUpdater(Context context, TunableTvView tvView, Menu menu) {
- mTvView = tvView;
+ public MenuUpdater(Menu menu, TunableTvView tvView, TvOptionsManager optionsManager) {
mMenu = menu;
+ mTvView = tvView;
+ mOptionsManager = optionsManager;
if (mTvView != null) {
mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() {
@Override
@@ -65,11 +75,18 @@ public class MenuUpdater {
}
});
}
+ if (mOptionsManager != null) {
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS,
+ mOptionChangeListener);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE,
+ mOptionChangeListener);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO,
+ mOptionChangeListener);
+ }
}
/**
- * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready
- * or not available any more.
+ * Sets the instance of {@link ChannelTuner}. Call this method when the channel tuner is ready.
*/
public void setChannelTuner(ChannelTuner channelTuner) {
if (mChannelTuner != null) {
@@ -79,7 +96,13 @@ public class MenuUpdater {
if (mChannelTuner != null) {
mChannelTuner.addListener(mChannelTunerListener);
}
- mMenu.update();
+ }
+
+ /**
+ * Called when the stream information changes.
+ */
+ public void onStreamInfoChanged() {
+ mMenu.update(TvOptionsRow.ID);
}
/**
@@ -92,5 +115,10 @@ public class MenuUpdater {
if (mTvView != null) {
mTvView.setOnScreenBlockedListener(null);
}
+ if (mOptionsManager != null) {
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_CLOSED_CAPTIONS, null);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_DISPLAY_MODE, null);
+ mOptionsManager.setOptionChangedListener(TvOptionsManager.OPTION_MULTI_AUDIO, null);
+ }
}
}
diff --git a/src/com/android/tv/menu/OptionsRowAdapter.java b/src/com/android/tv/menu/OptionsRowAdapter.java
index 93bd0a4d..dd6194a1 100644
--- a/src/com/android/tv/menu/OptionsRowAdapter.java
+++ b/src/com/android/tv/menu/OptionsRowAdapter.java
@@ -21,8 +21,6 @@ import android.view.View;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.TvOptionsManager;
-import com.android.tv.TvOptionsManager.OptionChangedListener;
import com.android.tv.analytics.Tracker;
import java.util.List;
@@ -66,12 +64,9 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
public void update() {
if (mActionList == null) {
mActionList = createActions();
- updateActions();
setItemList(mActionList);
} else {
- if (updateActions()) {
- setItemList(mActionList);
- }
+ updateActions();
}
}
@@ -81,7 +76,7 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
}
protected abstract List<MenuAction> createActions();
- protected abstract boolean updateActions();
+ protected abstract void updateActions();
protected abstract void executeAction(int type);
/**
@@ -93,37 +88,6 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
return mActionList.get(position);
}
- /**
- * Sets the action at the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void setAction(int position, MenuAction action) {
- mActionList.set(position, action);
- }
-
- /**
- * Adds an action to the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void addAction(int position, MenuAction action) {
- mActionList.add(position, action);
- }
-
- /**
- * Removes an action at the given position.
- * Note that action at the position may differ from returned by {@link #createActions}.
- * See {@link CustomizableOptionsRowAdapter}
- */
- protected void removeAction(int position) {
- mActionList.remove(position);
- }
-
- protected int getActionSize() {
- return mActionList.size();
- }
-
@Override
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
super.onBindViewHolder(viewHolder, position);
@@ -139,14 +103,4 @@ public abstract class OptionsRowAdapter extends ItemListRowView.ItemListAdapter<
// be preserved.
return mActionList.get(position).getType();
}
-
- protected void setOptionChangedListener(final MenuAction action) {
- TvOptionsManager om = getMainActivity().getTvOptionsManager();
- om.setOptionChangedListener(action.getType(), new OptionChangedListener() {
- @Override
- public void onOptionChanged(String newOption) {
- setItemList(mActionList);
- }
- });
- }
}
diff --git a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
index f3e09f80..c8249a4c 100644
--- a/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
+++ b/src/com/android/tv/menu/PartnerOptionsRowAdapter.java
@@ -38,8 +38,7 @@ public class PartnerOptionsRowAdapter extends CustomizableOptionsRowAdapter {
}
@Override
- protected boolean updateActions() {
+ protected void updateActions() {
// TODO: Support adding description for custom actions.
- return false;
}
}
diff --git a/src/com/android/tv/menu/PipOptionsRowAdapter.java b/src/com/android/tv/menu/PipOptionsRowAdapter.java
deleted file mode 100644
index 87203e9d..00000000
--- a/src/com/android/tv/menu/PipOptionsRowAdapter.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.menu;
-
-import android.content.Context;
-import android.text.TextUtils;
-
-import com.android.tv.MainActivity;
-import com.android.tv.R;
-import com.android.tv.TvOptionsManager;
-import com.android.tv.ui.TvViewUiManager;
-import com.android.tv.ui.sidepanel.PipInputSelectorFragment;
-import com.android.tv.util.PipInputManager.PipInput;
-import com.android.tv.util.TvSettings;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/*
- * An adapter of PIP options.
- */
-public class PipOptionsRowAdapter extends OptionsRowAdapter {
- private static final int[] DRAWABLE_ID_FOR_LAYOUT = {
- R.drawable.ic_pip_option_layout1,
- R.drawable.ic_pip_option_layout2,
- R.drawable.ic_pip_option_layout3,
- R.drawable.ic_pip_option_layout4,
- R.drawable.ic_pip_option_layout5 };
-
- private final TvOptionsManager mTvOptionsManager;
- private final TvViewUiManager mTvViewUiManager;
-
- public PipOptionsRowAdapter(Context context) {
- super(context);
- mTvOptionsManager = getMainActivity().getTvOptionsManager();
- mTvViewUiManager = getMainActivity().getTvViewUiManager();
- }
-
- @Override
- protected List<MenuAction> createActions() {
- List<MenuAction> actionList = new ArrayList<>();
- actionList.add(MenuAction.PIP_SELECT_INPUT_ACTION);
- actionList.add(MenuAction.PIP_SWAP_ACTION);
- actionList.add(MenuAction.PIP_SOUND_ACTION);
- actionList.add(MenuAction.PIP_LAYOUT_ACTION);
- actionList.add(MenuAction.PIP_SIZE_ACTION);
- for (MenuAction action : actionList) {
- setOptionChangedListener(action);
- }
- return actionList;
- }
-
- @Override
- public boolean updateActions() {
- boolean changed = false;
- if (updateSelectInputAction()) {
- changed = true;
- }
- if (updateLayoutAction()) {
- changed = true;
- }
- if (updateSizeAction()) {
- changed = true;
- }
- return changed;
- }
-
- private boolean updateSelectInputAction() {
- String oldInputLabel = mTvOptionsManager.getOptionString(TvOptionsManager.OPTION_PIP_INPUT);
-
- MainActivity tvActivity = getMainActivity();
- PipInput newInput = tvActivity.getPipInputManager().getPipInput(tvActivity.getPipChannel());
- String newInputLabel = newInput == null ? null : newInput.getLabel();
-
- if (!TextUtils.equals(oldInputLabel, newInputLabel)) {
- mTvOptionsManager.onPipInputChanged(newInputLabel);
- return true;
- }
- return false;
- }
-
- private boolean updateLayoutAction() {
- return MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(
- DRAWABLE_ID_FOR_LAYOUT[mTvViewUiManager.getPipLayout()]);
- }
-
- private boolean updateSizeAction() {
- boolean oldEnabled = MenuAction.PIP_SIZE_ACTION.isEnabled();
- boolean newEnabled = mTvViewUiManager.getPipLayout() != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE;
- if (oldEnabled != newEnabled) {
- MenuAction.PIP_SIZE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
- }
-
- @Override
- protected void executeAction(int type) {
- switch (type) {
- case TvOptionsManager.OPTION_PIP_INPUT:
- getMainActivity().getOverlayManager().getSideFragmentManager().show(
- new PipInputSelectorFragment());
- break;
- case TvOptionsManager.OPTION_PIP_SWAP:
- getMainActivity().swapPip();
- break;
- case TvOptionsManager.OPTION_PIP_SOUND:
- getMainActivity().togglePipSoundMode();
- break;
- case TvOptionsManager.OPTION_PIP_LAYOUT:
- int oldLayout = mTvViewUiManager.getPipLayout();
- int newLayout = (oldLayout + 1) % (TvSettings.PIP_LAYOUT_LAST + 1);
- mTvViewUiManager.setPipLayout(newLayout, true);
- MenuAction.PIP_LAYOUT_ACTION.setDrawableResId(DRAWABLE_ID_FOR_LAYOUT[newLayout]);
- break;
- case TvOptionsManager.OPTION_PIP_SIZE:
- int oldSize = mTvViewUiManager.getPipSize();
- int newSize = (oldSize + 1) % (TvSettings.PIP_SIZE_LAST + 1);
- mTvViewUiManager.setPipSize(newSize, true);
- break;
- }
- }
-}
diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java
index aff39db3..77715f28 100644
--- a/src/com/android/tv/menu/PlayControlsButton.java
+++ b/src/com/android/tv/menu/PlayControlsButton.java
@@ -39,6 +39,9 @@ public class PlayControlsButton extends FrameLayout {
private final int mIconColor;
private int mIconFocusedColor;
+ private int mImageResourceId;
+ private int mTintColor;
+
public PlayControlsButton(Context context) {
this(context, null);
}
@@ -67,10 +70,21 @@ public class PlayControlsButton extends FrameLayout {
* Sets the resource ID of the image to be displayed in the center of this control.
*/
public void setImageResId(int imageResId) {
- mIcon.setImageResource(imageResId);
- // Since on foucus changing, icons' color should be switched with animation,
+ int newTintColor = hasFocus() ? mIconFocusedColor : mIconColor;
+ if (mImageResourceId != imageResId) {
+ mImageResourceId = imageResId;
+ mIcon.setImageResource(imageResId);
+ updateTint(newTintColor);
+ } else if (newTintColor != mTintColor) {
+ updateTint(newTintColor);
+ }
+ }
+
+ private void updateTint(int tintColor) {
+ mTintColor = tintColor;
+ // Since on focus changing, icons' color should be switched with animation,
// as a result, selectors cannot be used to switch colors in this case.
- mIcon.getDrawable().setTint(hasFocus() ? mIconFocusedColor : mIconColor);
+ mIcon.getDrawable().setTint(tintColor);
}
/**
@@ -117,7 +131,9 @@ public class PlayControlsButton extends FrameLayout {
} else {
mIcon.setVisibility(View.GONE);
mLabel.setVisibility(View.VISIBLE);
- mLabel.setText(label);
+ if (!TextUtils.equals(mLabel.getText(), label)) {
+ mLabel.setText(label);
+ }
}
}
diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java
index a620d4dd..4d766788 100644
--- a/src/com/android/tv/menu/PlayControlsRowView.java
+++ b/src/com/android/tv/menu/PlayControlsRowView.java
@@ -18,10 +18,10 @@ package com.android.tv.menu;
import android.content.Context;
import android.content.res.Resources;
+import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
@@ -34,17 +34,16 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.data.Channel;
import com.android.tv.data.Program;
+import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
import com.android.tv.dvr.DvrManager;
-import com.android.tv.dvr.DvrUiHelper;
-import com.android.tv.dvr.ScheduledRecording;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
-import com.android.tv.dvr.ui.HalfSizedDialogFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.menu.Menu.MenuShowReason;
import com.android.tv.ui.TunableTvView;
-import com.android.tv.util.Utils;
public class PlayControlsRowView extends MenuRowView {
private static final int NORMAL_WIDTH_MAX_BUTTON_COUNT = 5;
@@ -53,14 +52,10 @@ public class PlayControlsRowView extends MenuRowView {
private final int mTimeTextLeftMargin;
private final int mTimelineWidth;
// Views
- private View mBackgroundView;
+ private TextView mBackgroundView;
private View mTimeIndicator;
private TextView mTimeText;
- private View mProgressEmptyBefore;
- private View mProgressWatched;
- private View mProgressBuffered;
- private View mProgressEmptyAfter;
- private View mControlBar;
+ private PlaybackProgressBar mProgress;
private PlayControlsButton mJumpPreviousButton;
private PlayControlsButton mRewindButton;
private PlayControlsButton mPlayPauseButton;
@@ -69,7 +64,6 @@ public class PlayControlsRowView extends MenuRowView {
private PlayControlsButton mRecordButton;
private TextView mProgramStartTimeText;
private TextView mProgramEndTimeText;
- private View mUnavailableMessageText;
private TunableTvView mTvView;
private TimeShiftManager mTimeShiftManager;
private final DvrDataManager mDvrDataManager;
@@ -83,6 +77,8 @@ public class PlayControlsRowView extends MenuRowView {
private final int mNormalButtonMargin;
private final int mCompactButtonMargin;
+ private final String mUnavailableMessage;
+
private final ScheduledRecordingListener mScheduledRecordingListener
= new ScheduledRecordingListener() {
@Override
@@ -138,6 +134,7 @@ public class PlayControlsRowView extends MenuRowView {
mDvrManager = null;
}
mMainActivity = (MainActivity) context;
+ mUnavailableMessage = res.getString(R.string.play_controls_unavailable);
}
@Override
@@ -171,14 +168,10 @@ public class PlayControlsRowView extends MenuRowView {
super.onFinishInflate();
// Clip the ViewGroup(body) to the rounded rectangle of outline.
findViewById(R.id.body).setClipToOutline(true);
- mBackgroundView = findViewById(R.id.background);
+ mBackgroundView = (TextView) findViewById(R.id.background);
mTimeIndicator = findViewById(R.id.time_indicator);
mTimeText = (TextView) findViewById(R.id.time_text);
- mProgressEmptyBefore = findViewById(R.id.timeline_bg_start);
- mProgressWatched = findViewById(R.id.watched);
- mProgressBuffered = findViewById(R.id.buffered);
- mProgressEmptyAfter = findViewById(R.id.timeline_bg_end);
- mControlBar = findViewById(R.id.play_control_bar);
+ mProgress = (PlaybackProgressBar) findViewById(R.id.progress);
mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous);
mRewindButton = (PlayControlsButton) findViewById(R.id.rewind);
mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause);
@@ -187,7 +180,6 @@ public class PlayControlsRowView extends MenuRowView {
mRecordButton = (PlayControlsButton) findViewById(R.id.record);
mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time);
mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time);
- mUnavailableMessageText = findViewById(R.id.unavailable_text);
initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous,
R.string.play_controls_description_skip_previous, null, new Runnable() {
@@ -195,7 +187,7 @@ public class PlayControlsRowView extends MenuRowView {
public void run() {
if (mTimeShiftManager.isAvailable()) {
mTimeShiftManager.jumpToPrevious();
- updateControls();
+ updateControls(true);
}
}
});
@@ -235,7 +227,7 @@ public class PlayControlsRowView extends MenuRowView {
public void run() {
if (mTimeShiftManager.isAvailable()) {
mTimeShiftManager.jumpToNext();
- updateControls();
+ updateControls(true);
}
}
});
@@ -265,18 +257,17 @@ public class PlayControlsRowView extends MenuRowView {
if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) {
Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel,
Toast.LENGTH_SHORT).show();
- } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity,
- currentChannel.getInputId())) {
+ } else {
Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager()
.getCurrentProgram(currentChannel.getId());
- if (program == null) {
- DvrUiHelper.showChannelRecordDurationOptions(mMainActivity, currentChannel);
- } else if (DvrUiHelper.handleCreateSchedule(mMainActivity, program)) {
- String msg = mMainActivity.getString(R.string.dvr_msg_current_program_scheduled,
- program.getTitle(),
- Utils.toTimeString(program.getEndTimeUtcMillis(), false));
- Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show();
- }
+ DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity,
+ currentChannel.getInputId(), new Runnable() {
+ @Override
+ public void run() {
+ DvrUiHelper.requestRecordingCurrentProgram(mMainActivity,
+ currentChannel, program, true);
+ }
+ });
}
} else if (currentChannel != null) {
DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(),
@@ -318,39 +309,37 @@ public class PlayControlsRowView extends MenuRowView {
@Override
public void onAvailabilityChanged() {
updateMenuVisibility();
- if (isShown()) {
- PlayControlsRowView.this.updateAll();
- }
+ PlayControlsRowView.this.updateAll(false);
}
@Override
public void onPlayStatusChanged(int status) {
updateMenuVisibility();
- if (mTimeShiftManager.isAvailable() && isShown()) {
- updateControls();
+ if (mTimeShiftManager.isAvailable()) {
+ updateControls(false);
}
}
@Override
public void onRecordTimeRangeChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
- updateControls();
+ if (mTimeShiftManager.isAvailable()) {
+ updateControls(false);
}
}
@Override
public void onCurrentPositionChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
+ if (mTimeShiftManager.isAvailable()) {
initializeTimeline();
- updateControls();
+ updateControls(false);
}
}
@Override
public void onProgramInfoChanged() {
- if (mTimeShiftManager.isAvailable() && isShown()) {
+ if (mTimeShiftManager.isAvailable()) {
initializeTimeline();
- updateControls();
+ updateControls(false);
}
}
@@ -372,7 +361,8 @@ public class PlayControlsRowView extends MenuRowView {
}
}
});
- updateAll();
+ // force update to initialize everything
+ updateAll(true);
}
private void initializeTimeline() {
@@ -380,6 +370,8 @@ public class PlayControlsRowView extends MenuRowView {
mTimeShiftManager.getCurrentPositionMs());
mProgramStartTimeMs = program.getStartTimeUtcMillis();
mProgramEndTimeMs = program.getEndTimeUtcMillis();
+ mProgress.setMax(mProgramEndTimeMs - mProgramStartTimeMs);
+ updateRecTimeText();
SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs);
}
@@ -389,10 +381,13 @@ public class PlayControlsRowView extends MenuRowView {
getMenu().setKeepVisible(keepMenuVisible);
}
+ public void onPreselected() {
+ updateControls(true);
+ }
+
@Override
public void onSelected(boolean showTitle) {
super.onSelected(showTitle);
- updateControls();
postHideRippleAnimation();
}
@@ -474,28 +469,32 @@ public class PlayControlsRowView extends MenuRowView {
* Updates the view contents. It is called from the PlayControlsRow.
*/
public void update() {
- updateAll();
+ updateAll(false);
}
- private void updateAll() {
+ private void updateAll(boolean forceUpdate) {
if (mTimeShiftManager.isAvailable() && !mTvView.isScreenBlocked()) {
setEnabled(true);
initializeTimeline();
mBackgroundView.setEnabled(true);
+ setTextIfNeeded(mBackgroundView, null);
} else {
setEnabled(false);
mBackgroundView.setEnabled(false);
+ setTextIfNeeded(mBackgroundView, mUnavailableMessage);
}
- updateControls();
+ // force the controls be updated no matter it's visible or not.
+ updateControls(forceUpdate);
}
- private void updateControls() {
- updateTime();
- updateProgress();
- updateRecTimeText();
- updateButtons();
- updateRecordButton();
- updateButtonMargin();
+ private void updateControls(boolean forceUpdate) {
+ if (forceUpdate || getContentsView().isShown()) {
+ updateTime();
+ updateProgress();
+ updateButtons();
+ updateRecordButton();
+ updateButtonMargin();
+ }
}
private void updateTime() {
@@ -504,70 +503,39 @@ public class PlayControlsRowView extends MenuRowView {
mTimeIndicator.setVisibility(View.VISIBLE);
} else {
mTimeText.setVisibility(View.INVISIBLE);
- mTimeIndicator.setVisibility(View.INVISIBLE);
+ mTimeIndicator.setVisibility(View.GONE);
return;
}
long currentPositionMs = mTimeShiftManager.getCurrentPositionMs();
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams();
int currentTimePositionPixel =
convertDurationToPixel(currentPositionMs - mProgramStartTimeMs);
- params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin;
- mTimeText.setLayoutParams(params);
- mTimeText.setText(getTimeString(currentPositionMs));
- params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams();
- params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin;
- mTimeIndicator.setLayoutParams(params);
+ mTimeText.setTranslationX(currentTimePositionPixel + mTimeTextLeftMargin);
+ setTextIfNeeded(mTimeText, getTimeString(currentPositionMs));
+ mTimeIndicator.setTranslationX(currentTimePositionPixel + mTimeIndicatorLeftMargin);
}
private void updateProgress() {
if (isEnabled()) {
- mProgressWatched.setVisibility(View.VISIBLE);
- mProgressBuffered.setVisibility(View.VISIBLE);
- mProgressEmptyAfter.setVisibility(View.VISIBLE);
- } else {
- mProgressWatched.setVisibility(View.INVISIBLE);
- mProgressBuffered.setVisibility(View.INVISIBLE);
- mProgressEmptyAfter.setVisibility(View.INVISIBLE);
- if (mProgramStartTimeMs < mProgramEndTimeMs) {
- layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs);
- } else {
- // Not initialized yet.
- layoutProgress(mProgressEmptyBefore, mTimelineWidth);
- }
- return;
- }
-
- long progressStartTimeMs = Math.min(mProgramEndTimeMs,
+ long progressStartTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs()));
- long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
+ long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs()));
- long progressEndTimeMs = Math.min(mProgramEndTimeMs,
+ long progressEndTimeMs = Math.min(mProgramEndTimeMs,
Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs()));
-
- layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs);
- layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs);
- layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs);
- }
-
- private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) {
- layoutProgress(progress, Math.max(0,
- convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1);
- }
-
- private void layoutProgress(View progress, int width) {
- ViewGroup.MarginLayoutParams params =
- (ViewGroup.MarginLayoutParams) progress.getLayoutParams();
- params.width = width;
- progress.setLayoutParams(params);
+ mProgress.setProgressRange(progressStartTimeMs - mProgramStartTimeMs,
+ progressEndTimeMs - mProgramStartTimeMs);
+ mProgress.setProgress(currentPlayingTimeMs - mProgramStartTimeMs);
+ } else {
+ mProgress.setProgressRange(0, 0);
+ }
}
private void updateRecTimeText() {
if (isEnabled()) {
mProgramStartTimeText.setVisibility(View.VISIBLE);
- mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs));
+ setTextIfNeeded(mProgramStartTimeText, getTimeString(mProgramStartTimeMs));
mProgramEndTimeText.setVisibility(View.VISIBLE);
- mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs));
+ setTextIfNeeded(mProgramEndTimeText, getTimeString(mProgramEndTimeMs));
} else {
mProgramStartTimeText.setVisibility(View.GONE);
mProgramEndTimeText.setVisibility(View.GONE);
@@ -576,11 +544,17 @@ public class PlayControlsRowView extends MenuRowView {
private void updateButtons() {
if (isEnabled()) {
- mControlBar.setVisibility(View.VISIBLE);
- mUnavailableMessageText.setVisibility(View.GONE);
+ mPlayPauseButton.setVisibility(View.VISIBLE);
+ mJumpPreviousButton.setVisibility(View.VISIBLE);
+ mJumpNextButton.setVisibility(View.VISIBLE);
+ mRewindButton.setVisibility(View.VISIBLE);
+ mFastForwardButton.setVisibility(View.VISIBLE);
} else {
- mControlBar.setVisibility(View.INVISIBLE);
- mUnavailableMessageText.setVisibility(View.VISIBLE);
+ mPlayPauseButton.setVisibility(View.GONE);
+ mJumpPreviousButton.setVisibility(View.GONE);
+ mJumpNextButton.setVisibility(View.GONE);
+ mRewindButton.setVisibility(View.GONE);
+ mFastForwardButton.setVisibility(View.GONE);
return;
}
@@ -622,6 +596,12 @@ public class PlayControlsRowView extends MenuRowView {
}
private void updateRecordButton() {
+ if (isEnabled()) {
+ mRecordButton.setVisibility(VISIBLE);
+ } else {
+ mRecordButton.setVisibility(GONE);
+ return;
+ }
if (!(mDvrManager != null
&& mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) {
mRecordButton.setVisibility(View.GONE);
@@ -682,4 +662,10 @@ public class PlayControlsRowView extends MenuRowView {
mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener);
}
}
+
+ private void setTextIfNeeded(TextView textView, String text) {
+ if (!TextUtils.equals(textView.getText(), text)) {
+ textView.setText(text);
+ }
+ }
}
diff --git a/src/com/android/tv/menu/PlaybackProgressBar.java b/src/com/android/tv/menu/PlaybackProgressBar.java
new file mode 100644
index 00000000..e8061bc6
--- /dev/null
+++ b/src/com/android/tv/menu/PlaybackProgressBar.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.menu;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.tv.R;
+
+/**
+ * A progress bar control which has two progresses which start in the middle of the control.
+ */
+public class PlaybackProgressBar extends View {
+ private final LayerDrawable mProgressDrawable;
+ private final Drawable mPrimaryDrawable;
+ private final Drawable mSecondaryDrawable;
+ private long mMax = 100;
+ private long mProgressStart = 0;
+ private long mProgressEnd = 0;
+ private long mProgress = 0;
+
+ public PlaybackProgressBar(Context context) {
+ this(context, null);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PlaybackProgressBar(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.PlaybackProgressBar, defStyleAttr, defStyleRes);
+ mProgressDrawable =
+ (LayerDrawable) a.getDrawable(R.styleable.PlaybackProgressBar_progressDrawable);
+ mPrimaryDrawable = mProgressDrawable.findDrawableByLayerId(android.R.id.progress);
+ mSecondaryDrawable =
+ mProgressDrawable.findDrawableByLayerId(android.R.id.secondaryProgress);
+ a.recycle();
+ refreshProgress();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mProgressDrawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ refreshProgress();
+ }
+
+ public void setMax(long max) {
+ if (max < 0) {
+ max = 0;
+ }
+ if (max != mMax) {
+ mMax = max;
+ if (mProgressStart > max) {
+ mProgressStart = max;
+ }
+ if (mProgressEnd > max) {
+ mProgressEnd = max;
+ }
+ if (mProgress > max) {
+ mProgress = max;
+ }
+ refreshProgress();
+ }
+ }
+
+ /**
+ * Sets the start and end position of the progress.
+ */
+ public void setProgressRange(long start, long end) {
+ start = constrain(start, 0, mMax);
+ end = constrain(end, start, mMax);
+ mProgress = constrain(mProgress, start, end);
+ if (start != mProgressStart || end != mProgressEnd) {
+ mProgressStart = start;
+ mProgressEnd = end;
+ setProgressLevels();
+ }
+ }
+
+ /**
+ * Sets the progress position.
+ */
+ public void setProgress(long progress) {
+ progress = constrain(progress, mProgressStart, mProgressEnd);
+ if (progress != mProgress) {
+ mProgress = progress;
+ setProgressLevels();
+ }
+ }
+
+ private long constrain(long value, long min, long max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ private void refreshProgress() {
+ int width = getWidth() - getPaddingStart() - getPaddingEnd();
+ int height = getHeight() - getPaddingTop() - getPaddingBottom();
+ mProgressDrawable.setBounds(0, 0, width, height);
+ setProgressLevels();
+ }
+
+ private void setProgressLevels() {
+ boolean progressUpdated = setProgressBound(mPrimaryDrawable, mProgressStart, mProgress);
+ progressUpdated |= setProgressBound(mSecondaryDrawable, mProgress, mProgressEnd);
+ if (progressUpdated) {
+ postInvalidate();
+ }
+ }
+
+ private boolean setProgressBound(Drawable drawable, long start, long end) {
+ Rect oldBounds = drawable.getBounds();
+ if (mMax == 0) {
+ if (!isEqualRect(oldBounds, 0, 0, 0, 0)) {
+ drawable.setBounds(0, 0, 0, 0);
+ return true;
+ }
+ return false;
+ }
+ int width = mProgressDrawable.getBounds().width();
+ int height = mProgressDrawable.getBounds().height();
+ int left = (int) (width * start / mMax);
+ int right = (int) (width * end / mMax);
+ if (!isEqualRect(oldBounds, left, 0, right, height)) {
+ drawable.setBounds(left, 0, right, height);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isEqualRect(Rect rect, int left, int top, int right, int bottom) {
+ return rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom;
+ }
+}
diff --git a/src/com/android/tv/menu/SimpleCardView.java b/src/com/android/tv/menu/SimpleCardView.java
index c99834be..fc5192da 100644
--- a/src/com/android/tv/menu/SimpleCardView.java
+++ b/src/com/android/tv/menu/SimpleCardView.java
@@ -19,12 +19,10 @@ package com.android.tv.menu;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.tv.data.Channel;
-
/**
* A view to render a guide card.
*/
-public class SimpleCardView extends BaseCardView<Channel> {
+public class SimpleCardView extends BaseCardView<ChannelsRowItem> {
public SimpleCardView(Context context) {
this(context, null, 0);
diff --git a/src/com/android/tv/menu/TvOptionsRowAdapter.java b/src/com/android/tv/menu/TvOptionsRowAdapter.java
index fb062246..6e035f22 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,7 @@ 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 com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -39,12 +38,6 @@ import java.util.List;
* An adapter of options.
*/
public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
- private static final boolean ENABLE_IN_APP_PIP = false;
-
- private int mPositionPipAction;
- // If mInAppPipAction is false, system-wide PIP is used.
- private boolean mInAppPipAction = true;
-
public TvOptionsRowAdapter(Context context, List<CustomAction> customActions) {
super(context, customActions);
}
@@ -53,123 +46,73 @@ public class TvOptionsRowAdapter extends CustomizableOptionsRowAdapter {
protected List<MenuAction> createBaseActions() {
List<MenuAction> actionList = new ArrayList<>();
actionList.add(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
- setOptionChangedListener(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
actionList.add(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- setOptionChangedListener(MenuAction.SELECT_DISPLAY_MODE_ACTION);
- actionList.add(MenuAction.PIP_IN_APP_ACTION);
- setOptionChangedListener(MenuAction.PIP_IN_APP_ACTION);
- mPositionPipAction = actionList.size() - 1;
+ if (Features.PICTURE_IN_PICTURE.isEnabled(getMainActivity())) {
+ actionList.add(MenuAction.SYSTEMWIDE_PIP_ACTION);
+ }
actionList.add(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
- setOptionChangedListener(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
actionList.add(MenuAction.MORE_CHANNELS_ACTION);
- if (DeveloperOptionFragment.shouldShow()) {
+ if (Utils.isDeveloper()) {
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();
+ updatePipAction();
+ updateMultiAudioAction();
+ updateDisplayModeAction();
return actionList;
}
@Override
- protected boolean updateActions() {
- boolean changed = false;
+ protected void updateActions() {
+ if (updateClosedCaptionAction()) {
+ notifyItemChanged(getItemPosition(MenuAction.SELECT_CLOSED_CAPTION_ACTION));
+ }
if (updatePipAction()) {
- changed = true;
+ notifyItemChanged(getItemPosition(MenuAction.SYSTEMWIDE_PIP_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;
- }
+ @VisibleForTesting
+ private boolean updateClosedCaptionAction() {
+ return updateActionDescription(MenuAction.SELECT_CLOSED_CAPTION_ACTION);
+ }
- // 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;
- }
+ private boolean updatePipAction() {
+ if (containsItem(MenuAction.SYSTEMWIDE_PIP_ACTION)) {
+ return MenuAction.setEnabled(MenuAction.SYSTEMWIDE_PIP_ACTION,
+ !getMainActivity().isScreenBlockedByResourceConflictOrParentalControl());
}
-
- // 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;
+ return false;
}
- @VisibleForTesting
boolean updateMultiAudioAction() {
List<TvTrackInfo> audioTracks = getMainActivity().getTracks(TvTrackInfo.TYPE_AUDIO);
- boolean oldEnabled = MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled();
- boolean newEnabled = audioTracks != null && audioTracks.size() > 1;
- if (oldEnabled != newEnabled) {
- MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
+ boolean enabled = audioTracks != null && audioTracks.size() > 1;
+ // Use "|" operator for non-short-circuit evaluation.
+ return MenuAction.setEnabled(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION, enabled)
+ | updateActionDescription(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION);
}
private boolean updateDisplayModeAction() {
TvViewUiManager uiManager = getMainActivity().getTvViewUiManager();
- boolean oldEnabled = MenuAction.SELECT_DISPLAY_MODE_ACTION.isEnabled();
- boolean newEnabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL)
+ boolean enabled = uiManager.isDisplayModeAvailable(DisplayMode.MODE_FULL)
|| uiManager.isDisplayModeAvailable(DisplayMode.MODE_ZOOM);
- if (oldEnabled != newEnabled) {
- MenuAction.SELECT_DISPLAY_MODE_ACTION.setEnabled(newEnabled);
- return true;
- }
- return false;
+ // Use "|" operator for non-short-circuit evaluation.
+ return MenuAction.setEnabled(MenuAction.SELECT_DISPLAY_MODE_ACTION, enabled)
+ | updateActionDescription(MenuAction.SELECT_DISPLAY_MODE_ACTION);
+ }
+
+ private boolean updateActionDescription(MenuAction action) {
+ return MenuAction.setActionDescription(action,
+ getMainActivity().getTvOptionsManager().getOptionString(action.getType()));
}
@Override
@@ -183,9 +126,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 +145,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/parental/ContentRatingSystem.java b/src/com/android/tv/parental/ContentRatingSystem.java
index 6b5d6635..5672b793 100644
--- a/src/com/android/tv/parental/ContentRatingSystem.java
+++ b/src/com/android/tv/parental/ContentRatingSystem.java
@@ -110,6 +110,15 @@ public class ContentRatingSystem {
return mRatings;
}
+ public Rating getRating(String name) {
+ for (Rating rating : mRatings) {
+ if (TextUtils.equals(rating.getName(), name)) {
+ return rating;
+ }
+ }
+ return null;
+ }
+
public List<SubRating> getSubRatings(){
return mSubRatings;
}
diff --git a/src/com/android/tv/parental/ContentRatingsManager.java b/src/com/android/tv/parental/ContentRatingsManager.java
index 57c25f48..c3bb8c0f 100644
--- a/src/com/android/tv/parental/ContentRatingsManager.java
+++ b/src/com/android/tv/parental/ContentRatingsManager.java
@@ -20,7 +20,10 @@ import android.content.Context;
import android.media.tv.TvContentRating;
import android.media.tv.TvContentRatingSystemInfo;
import android.media.tv.TvInputManager;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.tv.R;
import com.android.tv.parental.ContentRatingSystem.Rating;
import com.android.tv.parental.ContentRatingSystem.SubRating;
@@ -53,6 +56,19 @@ public class ContentRatingsManager {
}
/**
+ * Returns the content rating system with the give ID.
+ */
+ @Nullable
+ public ContentRatingSystem getContentRatingSystem(String contentRatingSystemId) {
+ for (ContentRatingSystem ratingSystem : mContentRatingSystems) {
+ if (TextUtils.equals(ratingSystem.getId(), contentRatingSystemId)) {
+ return ratingSystem;
+ }
+ }
+ return null;
+ }
+
+ /**
* Returns a new list of all content rating systems defined.
*/
public List<ContentRatingSystem> getContentRatingSystems() {
@@ -64,6 +80,9 @@ public class ContentRatingsManager {
* displayed to the user. For example, "TV-PG (L, S)".
*/
public String getDisplayNameForRating(TvContentRating canonicalRating) {
+ if (TvContentRating.UNRATED.equals(canonicalRating)) {
+ return mContext.getResources().getString(R.string.unrated_rating_name);
+ }
Rating rating = getRating(canonicalRating);
if (rating == null) {
return null;
diff --git a/src/com/android/tv/parental/ParentalControlSettings.java b/src/com/android/tv/parental/ParentalControlSettings.java
index d7e1846e..2471c565 100644
--- a/src/com/android/tv/parental/ParentalControlSettings.java
+++ b/src/com/android/tv/parental/ParentalControlSettings.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
+import com.android.tv.experiments.Experiments;
import com.android.tv.parental.ContentRatingSystem.Rating;
import com.android.tv.parental.ContentRatingSystem.SubRating;
import com.android.tv.util.TvSettings;
@@ -109,6 +110,10 @@ public class ParentalControlSettings {
@ContentRatingLevel int currentLevel = getContentRatingLevel();
if (currentLevel != TvSettings.CONTENT_RATING_LEVEL_CUSTOM) {
mRatings = ContentRatingLevelPolicy.getRatingsForLevel(this, manager, currentLevel);
+ if (currentLevel != TvSettings.CONTENT_RATING_LEVEL_NONE) {
+ // UNRATED contents should be blocked unless the rating level is none or custom
+ mRatings.add(TvContentRating.UNRATED);
+ }
storeRatings();
}
}
@@ -129,6 +134,11 @@ public class ParentalControlSettings {
}
} else {
mRatings = ContentRatingLevelPolicy.getRatingsForLevel(this, manager, level);
+ if (level != TvSettings.CONTENT_RATING_LEVEL_NONE
+ && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+ // UNRATED contents should be blocked unless the rating level is none or custom
+ mRatings.add(TvContentRating.UNRATED);
+ }
}
storeRatings();
}
@@ -138,6 +148,23 @@ public class ParentalControlSettings {
return TvSettings.getContentRatingLevel(mContext);
}
+ /** Sets the blocked status of a unrated contents. */
+ public boolean setUnratedBlocked(boolean blocked) {
+ boolean changed;
+ if (blocked) {
+ changed = mRatings.add(TvContentRating.UNRATED);
+ mTvInputManager.addBlockedRating(TvContentRating.UNRATED);
+ } else {
+ changed = mRatings.remove(TvContentRating.UNRATED);
+ mTvInputManager.removeBlockedRating(TvContentRating.UNRATED);
+ }
+ if (changed) {
+ // change to custom level if the blocked status is changed
+ changeToCustomLevel();
+ }
+ return changed;
+ }
+
/**
* Sets the blocked status of a given content rating.
* <p>
@@ -172,8 +199,10 @@ public class ParentalControlSettings {
* @return The {@link TvContentRating} that is blocked.
*/
public TvContentRating getBlockedRating(TvContentRating[] ratings) {
- if (ratings == null) {
- return null;
+ if (ratings == null || ratings.length <= 0) {
+ return mTvInputManager.isRatingBlocked(TvContentRating.UNRATED)
+ ? TvContentRating.UNRATED
+ : null;
}
for (TvContentRating rating : ratings) {
if (mTvInputManager.isRatingBlocked(rating)) {
diff --git a/src/com/android/tv/perf/EventNames.java b/src/com/android/tv/perf/EventNames.java
new file mode 100644
index 00000000..6026897b
--- /dev/null
+++ b/src/com/android/tv/perf/EventNames.java
@@ -0,0 +1,56 @@
+/*
+ * 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.perf;
+
+import android.support.annotation.StringDef;
+
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Constants for performance event names.
+ *
+ * <p>Only constants are used to insure no PII is sent.
+ *
+ */
+public final class EventNames {
+
+ @Retention(SOURCE)
+ @StringDef({
+ APPLICATION_ONCREATE,
+ FETCH_EPG_TASK,
+ MAIN_ACTIVITY_ONCREATE,
+ MAIN_ACTIVITY_ONSTART,
+ MAIN_ACTIVITY_ONRESUME,
+ ON_DEVICE_SEARCH
+ })
+ public @interface EventName {}
+
+ public static final String APPLICATION_ONCREATE = "Application.onCreate";
+ public static final String FETCH_EPG_TASK = "FetchEpgTask";
+ public static final String MAIN_ACTIVITY_ONCREATE = "MainActivity.onCreate";
+ public static final String MAIN_ACTIVITY_ONSTART = "MainActivity.onStart";
+ public static final String MAIN_ACTIVITY_ONRESUME = "MainActivity.onResume";
+ /**
+ * Event name for query running time of on-device search in
+ * {@link com.android.tv.search.LocalSearchProvider}.
+ */
+ public static final String ON_DEVICE_SEARCH = "OnDeviceSearch";
+
+ private EventNames() {}
+}
diff --git a/src/com/android/tv/perf/PerformanceMonitor.java b/src/com/android/tv/perf/PerformanceMonitor.java
new file mode 100644
index 00000000..40368b41
--- /dev/null
+++ b/src/com/android/tv/perf/PerformanceMonitor.java
@@ -0,0 +1,99 @@
+/*
+ * 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.perf;
+
+import android.content.Context;
+
+import static com.android.tv.perf.EventNames.EventName;
+
+/** Measures Performance. */
+public interface PerformanceMonitor {
+
+ /**
+ * Starts monitoring application's lifecylce for interesting memory events, captures and records
+ * memory usage data whenever these events are fired.
+ */
+ void startMemoryMonitor();
+
+ /**
+ * Collects and records memory usage for a specific custom event
+ *
+ * @param eventName to record
+ */
+ void recordMemory(@EventName String eventName);
+
+ /**
+ * Starts a timer for a global event to allow measuring the event's latency across activities If
+ * multiple events with the same name are started, only the last event is retained.
+ *
+ * @param eventName for which the timer starts
+ */
+ void startGlobalTimer(@EventName String eventName);
+
+ /**
+ * Stops a cross activities timer for a specific eventName and records the timer duration. If no
+ * timer found for the event specified an error will be logged, and recording will be skipped.
+ *
+ * @param eventName for which the timer stops
+ */
+ void stopGlobalTimer(@EventName String eventName);
+
+ /**
+ * Starts a timer to record latency of a specific scenario or event. Use this method to track
+ * latency in the same method/class
+ *
+ * @return TimerEvent object to be used for stopping/recording the timer for a specific event.
+ * If PerformanceMonitor is not initialized for any reason, an empty TimerEvent will be
+ * returned.
+ */
+ TimerEvent startTimer();
+
+
+ /**
+ * Stops timer for a specific event and records the timer duration. passing a null TimerEvent
+ * will cause this operation to be skipped.
+ *
+ * @param event that needs to be stopped
+ * @param eventName for which the timer stops. This must be constant with no PII.
+ */
+ void stopTimer(TimerEvent event, @EventName String eventName);
+
+ /**
+ * Starts recording jank for a specific scenario or event.
+ *
+ * <p>If jank recording was started already for an event with the current name, but was never
+ * stopped, the previously recorded event will be skipped.
+ *
+ * @param eventName of the event for which tracking is started
+ */
+ void startJankRecorder(@EventName String eventName);
+
+ /**
+ * Stops recording jank for a specific event and records the jank event.
+ *
+ * @param eventName of the event that needs to be stopped
+ */
+ void stopJankRecorder(@EventName String eventName);
+
+ /**
+ * Starts activity to display PerformanceMonitor events recorded in local database for debug
+ * purpose.
+ *
+ * @return true if the activity is available to start
+ */
+ boolean startPerformanceMonitorEventDebugActivity(Context context);
+}
diff --git a/src/com/android/tv/perf/StubPerformanceMonitor.java b/src/com/android/tv/perf/StubPerformanceMonitor.java
new file mode 100644
index 00000000..3742a2a7
--- /dev/null
+++ b/src/com/android/tv/perf/StubPerformanceMonitor.java
@@ -0,0 +1,65 @@
+/*
+ * 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.perf;
+
+import android.app.Application;
+import android.content.Context;
+
+/** Do nothing implementation of {@link PerformanceMonitor}. */
+public final class StubPerformanceMonitor implements PerformanceMonitor {
+
+ private static final TimerEvent TIMER_EVENT = new TimerEvent() {};
+
+ public static PerformanceMonitor initialize(Application app) {
+ return new StubPerformanceMonitor();
+ }
+
+ @Override
+ public void startMemoryMonitor() {}
+
+ @Override
+ public void recordMemory(String customEventName) {}
+
+ @Override
+ public void startGlobalTimer(String customEventName) {}
+
+ @Override
+ public void stopGlobalTimer(String customEventName) {}
+
+ @Override
+ public TimerEvent startTimer() {
+ return TIMER_EVENT;
+ }
+
+ @Override
+ public void stopTimer(TimerEvent event, String name) {}
+
+ @Override
+ public void startJankRecorder(String eventName) {}
+
+ @Override
+ public void stopJankRecorder(String eventName) {}
+
+ @Override
+ public boolean startPerformanceMonitorEventDebugActivity(Context context) {
+ return false;
+ }
+
+ public static TimerEvent startBootstrapTimer() {
+ return new TimerEvent() {};
+ }
+}
diff --git a/src/com/android/tv/perf/TimerEvent.java b/src/com/android/tv/perf/TimerEvent.java
new file mode 100644
index 00000000..f8ac6b2d
--- /dev/null
+++ b/src/com/android/tv/perf/TimerEvent.java
@@ -0,0 +1,20 @@
+/*
+ * 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.perf;
+
+/** An event to time */
+public interface TimerEvent {}
diff --git a/src/com/android/tv/receiver/BootCompletedReceiver.java b/src/com/android/tv/receiver/BootCompletedReceiver.java
index 8d6c5a14..369e7d54 100644
--- a/src/com/android/tv/receiver/BootCompletedReceiver.java
+++ b/src/com/android/tv/receiver/BootCompletedReceiver.java
@@ -21,13 +21,15 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.os.Build;
import android.util.Log;
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.dvr.recorder.RecordingScheduler;
+import com.android.tv.recommendation.ChannelPreviewUpdater;
import com.android.tv.recommendation.NotificationService;
import com.android.tv.util.OnboardingUtils;
import com.android.tv.util.SetupUtils;
@@ -49,12 +51,20 @@ public class BootCompletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ return;
+ }
if (DEBUG) Log.d(TAG, "boot completed " + intent);
TvApplication.setCurrentRunningProcess(context, true);
- // Start {@link NotificationService}.
- Intent notificationIntent = new Intent(context, NotificationService.class);
- notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
- context.startService(notificationIntent);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ChannelPreviewUpdater.getInstance(context).updatePreviewDataForChannelsImmediately();
+ } else {
+ Intent notificationIntent = new Intent(context, NotificationService.class);
+ notificationIntent.setAction(NotificationService.ACTION_SHOW_RECOMMENDATION);
+ context.startService(notificationIntent);
+ }
// Grant permission to already set up packages after the system has finished booting.
SetupUtils.grantEpgPermissionToSetUpPackages(context);
@@ -74,8 +84,9 @@ public class BootCompletedReceiver extends BroadcastReceiver {
}
}
- if (CommonFeatures.DVR.isEnabled(context)) {
- DvrRecordingService.startService(context);
+ RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler();
+ if (scheduler != null) {
+ scheduler.updateAndStartServiceIfNeeded();
}
}
}
diff --git a/src/com/android/tv/receiver/GlobalKeyReceiver.java b/src/com/android/tv/receiver/GlobalKeyReceiver.java
index 8cd4fdf1..cc8e76c4 100644
--- a/src/com/android/tv/receiver/GlobalKeyReceiver.java
+++ b/src/com/android/tv/receiver/GlobalKeyReceiver.java
@@ -19,7 +19,8 @@ package com.android.tv.receiver;
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 +32,64 @@ 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 long sLastEventTime;
+ private static boolean sUserSetupComplete;
@Override
public void onReceive(Context context, Intent intent) {
+ if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ return;
+ }
TvApplication.setCurrentRunningProcess(context, true);
+ Context appContext = context.getApplicationContext();
+ if (DEBUG) Log.d(TAG, "onReceive: " + intent);
+ if (sUserSetupComplete) {
+ handleIntent(appContext, intent);
+ } else {
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ return Settings.Secure.getInt(appContext.getContentResolver(),
+ SETTINGS_USER_SETUP_COMPLETE, 0) != 0;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean setupComplete) {
+ if (DEBUG) Log.d(TAG, "Is setup complete: " + setupComplete);
+ sUserSetupComplete = setupComplete;
+ if (sUserSetupComplete) {
+ handleIntent(appContext, intent);
+ }
+ }
+ }.execute();
+ }
+ }
+
+ private void handleIntent(Context appContext, Intent intent) {
if (ACTION_GLOBAL_BUTTON.equals(intent.getAction())) {
KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
- if (DEBUG) Log.d(TAG, "onReceive: " + event);
+ if (DEBUG) Log.d(TAG, "handleIntent: " + event);
int keyCode = event.getKeyCode();
int action = event.getAction();
- if (action == KeyEvent.ACTION_UP) {
+ long eventTime = event.getEventTime();
+ if (action == KeyEvent.ACTION_UP && sLastEventTime != eventTime) {
+ // Workaround for b/23947504, the same key event may be sent twice, filter it.
+ sLastEventTime = eventTime;
switch (keyCode) {
case KeyEvent.KEYCODE_GUIDE:
- context.startActivity(
- new Intent(Intent.ACTION_VIEW, TvContract.Programs.CONTENT_URI));
+ ((TvApplication) appContext).handleGuideKey();
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..124172f0 100644
--- a/src/com/android/tv/receiver/PackageIntentsReceiver.java
+++ b/src/com/android/tv/receiver/PackageIntentsReceiver.java
@@ -19,17 +19,29 @@ package com.android.tv.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
import com.android.tv.TvApplication;
+import com.android.tv.util.Partner;
/**
* A class for handling the broadcast intents from PackageManager.
*/
public class PackageIntentsReceiver extends BroadcastReceiver {
+ private static final String TAG = "PackageIntentsReceiver";
@Override
public void onReceive(Context context, Intent intent) {
+ if (!TvApplication.getSingletons(context).getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ return;
+ }
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/ChannelPreviewUpdater.java b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
new file mode 100644
index 00000000..2709ebe1
--- /dev/null
+++ b/src/com/android/tv/recommendation/ChannelPreviewUpdater.java
@@ -0,0 +1,323 @@
+/*
+ * 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.recommendation;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.support.media.tv.TvContractCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.PreviewDataManager;
+import com.android.tv.data.PreviewProgramContent;
+import com.android.tv.data.Program;
+import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/** Class for updating the preview programs for {@link Channel}. */
+@RequiresApi(Build.VERSION_CODES.O)
+public class ChannelPreviewUpdater {
+ private static final String TAG = "ChannelPreviewUpdater";
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
+
+ private static final int UPATE_PREVIEW_PROGRAMS_JOB_ID = 1000001;
+ private static final long ROUTINE_INTERVAL_MS = TimeUnit.MINUTES.toMillis(10);
+ // The left time of a program should meet the threshold so that it could be recommended.
+ private static final long RECOMMENDATION_THRESHOLD_LEFT_TIME_MS =
+ TimeUnit.MINUTES.toMillis(10);
+ private static final int RECOMMENDATION_THRESHOLD_PROGRESS = 90; // 90%
+ private static final int RECOMMENDATION_COUNT = 6;
+ private static final int MIN_COUNT_TO_ADD_ROW = 4;
+
+ private static ChannelPreviewUpdater sChannelPreviewUpdater;
+
+ /**
+ * Creates and returns the {@link ChannelPreviewUpdater}.
+ */
+ public static ChannelPreviewUpdater getInstance(Context context) {
+ if (sChannelPreviewUpdater == null) {
+ sChannelPreviewUpdater = new ChannelPreviewUpdater(context.getApplicationContext());
+ }
+ return sChannelPreviewUpdater;
+ }
+
+ private final Context mContext;
+ private final Recommender mRecommender;
+ private final PreviewDataManager mPreviewDataManager;
+ private JobService mJobService;
+ private JobParameters mJobParams;
+
+ private final ParentalControlSettings mParentalControlSettings;
+
+ private boolean mNeedUpdateAfterRecommenderReady = false;
+
+ private Recommender.Listener mRecommenderListener = new Recommender.Listener() {
+ @Override
+ public void onRecommenderReady() {
+ if (mNeedUpdateAfterRecommenderReady) {
+ if (DEBUG) Log.d(TAG, "Recommender is ready");
+ updatePreviewDataForChannelsImmediately();
+ mNeedUpdateAfterRecommenderReady = false;
+ }
+ }
+
+ @Override
+ public void onRecommendationChanged() {
+ updatePreviewDataForChannelsImmediately();
+ }
+ };
+
+ private ChannelPreviewUpdater(Context context) {
+ mContext = context;
+ mRecommender = new Recommender(context, mRecommenderListener, true);
+ mRecommender.registerEvaluator(new RandomEvaluator(), 0.1, 0.1);
+ mRecommender.registerEvaluator(new FavoriteChannelEvaluator(), 0.5, 0.5);
+ mRecommender.registerEvaluator(new RoutineWatchEvaluator(), 1.0, 1.0);
+ ApplicationSingletons appSingleton = TvApplication.getSingletons(context);
+ mPreviewDataManager = appSingleton.getPreviewDataManager();
+ mParentalControlSettings = appSingleton.getTvInputManagerHelper()
+ .getParentalControlSettings();
+ }
+
+ /**
+ * Starts the routine service for updating the preview programs.
+ */
+ public void startRoutineService() {
+ JobScheduler jobScheduler =
+ (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (jobScheduler.getPendingJob(UPATE_PREVIEW_PROGRAMS_JOB_ID) != null) {
+ if (DEBUG) Log.d(TAG, "UPDATE_PREVIEW_JOB already exists");
+ return;
+ }
+ JobInfo job = new JobInfo.Builder(UPATE_PREVIEW_PROGRAMS_JOB_ID,
+ new ComponentName(mContext, ChannelPreviewUpdateService.class))
+ .setPeriodic(ROUTINE_INTERVAL_MS)
+ .setPersisted(true)
+ .build();
+ if (jobScheduler.schedule(job) < 0) {
+ Log.i(TAG, "JobScheduler failed to schedule the job");
+ }
+ }
+
+ /** Called when {@link ChannelPreviewUpdateService} is started. */
+ void onStartJob(JobService service, JobParameters params) {
+ if (DEBUG) Log.d(TAG, "onStartJob");
+ mJobService = service;
+ mJobParams = params;
+ updatePreviewDataForChannelsImmediately();
+ }
+
+ /**
+ * Updates the preview programs table.
+ */
+ public void updatePreviewDataForChannelsImmediately() {
+ if (!mRecommender.isReady()) {
+ mNeedUpdateAfterRecommenderReady = true;
+ return;
+ }
+
+ if (!mPreviewDataManager.isLoadFinished()) {
+ mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() {
+ @Override
+ public void onPreviewDataLoadFinished() {
+ mPreviewDataManager.removeListener(this);
+ updatePreviewDataForChannels();
+ }
+
+ @Override
+ public void onPreviewDataUpdateFinished() { }
+ });
+ return;
+ }
+ updatePreviewDataForChannels();
+ }
+
+ /** Called when {@link ChannelPreviewUpdateService} is stopped. */
+ void onStopJob() {
+ if (DEBUG) Log.d(TAG, "onStopJob");
+ mJobService = null;
+ mJobParams = null;
+ }
+
+ private void updatePreviewDataForChannels() {
+ new AsyncTask<Void, Void, Set<Program>>() {
+ @Override
+ protected Set<Program> doInBackground(Void... params) {
+ Set<Program> programs = new HashSet<>();
+ List<Channel> channels = new ArrayList<>(mRecommender.recommendChannels());
+ for (Channel channel : channels) {
+ if (channel.isPhysicalTunerChannel()) {
+ final Program program = Utils.getCurrentProgram(mContext, channel.getId());
+ if (program != null
+ && isChannelRecommendationApplicable(channel, program)) {
+ programs.add(program);
+ if (programs.size() >= RECOMMENDATION_COUNT) {
+ break;
+ }
+ }
+ }
+ }
+ return programs;
+ }
+
+ private boolean isChannelRecommendationApplicable(Channel channel, Program program) {
+ final long programDurationMs =
+ program.getEndTimeUtcMillis() - program.getStartTimeUtcMillis();
+ if (programDurationMs <= 0) {
+ return false;
+ }
+ if (TextUtils.isEmpty(program.getPosterArtUri())) {
+ return false;
+ }
+ if (mParentalControlSettings.isParentalControlsEnabled()
+ && (channel.isLocked()
+ || mParentalControlSettings.isRatingBlocked(
+ program.getContentRatings()))) {
+ return false;
+ }
+ long programLeftTimsMs = program.getEndTimeUtcMillis() - System.currentTimeMillis();
+ final int programProgress =
+ (programDurationMs <= 0)
+ ? -1
+ : 100 - (int) (programLeftTimsMs * 100 / programDurationMs);
+
+ // We recommend those programs that meet the condition only.
+ return programProgress < RECOMMENDATION_THRESHOLD_PROGRESS
+ || programLeftTimsMs > RECOMMENDATION_THRESHOLD_LEFT_TIME_MS;
+ }
+
+ @Override
+ protected void onPostExecute(Set<Program> programs) {
+ updatePreviewDataForChannelsInternal(programs);
+ }
+ }.execute();
+ }
+
+ private void updatePreviewDataForChannelsInternal(Set<Program> programs) {
+ long defaultPreviewChannelId = mPreviewDataManager.getPreviewChannelId(
+ PreviewDataManager.TYPE_DEFAULT_PREVIEW_CHANNEL);
+ if (defaultPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
+ // Only create if there is enough programs
+ if (programs.size() > MIN_COUNT_TO_ADD_ROW) {
+ mPreviewDataManager.createDefaultPreviewChannel(
+ new PreviewDataManager.OnPreviewChannelCreationResultListener() {
+ @Override
+ public void onPreviewChannelCreationResult(
+ long createdPreviewChannelId) {
+ if (createdPreviewChannelId
+ != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
+ TvContractCompat.requestChannelBrowsable(
+ mContext, createdPreviewChannelId);
+ updatePreviewProgramsForPreviewChannel(
+ createdPreviewChannelId,
+ generatePreviewProgramContentsFromPrograms(
+ createdPreviewChannelId, programs));
+ }
+ }
+ });
+ }
+ } else {
+ updatePreviewProgramsForPreviewChannel(defaultPreviewChannelId,
+ generatePreviewProgramContentsFromPrograms(defaultPreviewChannelId, programs));
+ }
+ }
+
+ private Set<PreviewProgramContent> generatePreviewProgramContentsFromPrograms(
+ long previewChannelId, Set<Program> programs) {
+ Set<PreviewProgramContent> result = new HashSet<>();
+ for (Program program : programs) {
+ PreviewProgramContent previewProgramContent =
+ PreviewProgramContent.createFromProgram(mContext, previewChannelId, program);
+ if (previewProgramContent != null) {
+ result.add(previewProgramContent);
+ }
+ }
+ return result;
+ }
+
+ private void updatePreviewProgramsForPreviewChannel(long previewChannelId,
+ Set<PreviewProgramContent> previewProgramContents) {
+ PreviewDataManager.PreviewDataListener previewDataListener
+ = new PreviewDataManager.PreviewDataListener() {
+ @Override
+ public void onPreviewDataLoadFinished() { }
+
+ @Override
+ public void onPreviewDataUpdateFinished() {
+ mPreviewDataManager.removeListener(this);
+ if (mJobService != null && mJobParams != null) {
+ if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute with JobService");
+ mJobService.jobFinished(mJobParams, false);
+ mJobService = null;
+ mJobParams = null;
+ } else {
+ if (DEBUG) Log.d(TAG, "UpdateAsyncTask.onPostExecute without JobService");
+ }
+ }
+ };
+ mPreviewDataManager.updatePreviewProgramsForChannel(
+ previewChannelId, previewProgramContents, previewDataListener);
+ }
+
+ /**
+ * Job to execute the update of preview programs.
+ */
+ public static class ChannelPreviewUpdateService extends JobService {
+ private ChannelPreviewUpdater mChannelPreviewUpdater;
+
+ @Override
+ public void onCreate() {
+ TvApplication.setCurrentRunningProcess(this, true);
+ if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onCreate");
+ mChannelPreviewUpdater = ChannelPreviewUpdater.getInstance(this);
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ mChannelPreviewUpdater.onStartJob(this, params);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ mChannelPreviewUpdater.onStopJob();
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "ChannelPreviewUpdateService.onDestroy");
+ }
+ }
+}
diff --git a/src/com/android/tv/recommendation/NotificationService.java b/src/com/android/tv/recommendation/NotificationService.java
index 30ec73e3..a44eca41 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.
@@ -455,11 +456,17 @@ public class NotificationService extends Service implements Recommender.Listener
}
private Bitmap overlayChannelLogo(Bitmap logo, Bitmap background) {
- Bitmap result = BitmapUtils.scaleBitmap(
+ Bitmap result = BitmapUtils.getScaledMutableBitmap(
background, Integer.MAX_VALUE, mCardImageHeight);
Bitmap scaledLogo = BitmapUtils.scaleBitmap(
logo, mChannelLogoMaxWidth, mChannelLogoMaxHeight);
- Canvas canvas = new Canvas(result);
+ Canvas canvas;
+ try {
+ canvas = new Canvas(result);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to create Canvas", e);
+ return background;
+ }
canvas.drawBitmap(result, new Matrix(), null);
Rect rect = new Rect();
int startPadding;
diff --git a/src/com/android/tv/recommendation/RecommendationDataManager.java b/src/com/android/tv/recommendation/RecommendationDataManager.java
index 62ccd578..dc148ec8 100644
--- a/src/com/android/tv/recommendation/RecommendationDataManager.java
+++ b/src/com/android/tv/recommendation/RecommendationDataManager.java
@@ -41,7 +41,7 @@ import com.android.tv.data.ChannelDataManager;
import com.android.tv.data.Program;
import com.android.tv.data.WatchedHistoryManager;
import com.android.tv.util.PermissionUtils;
-import com.android.tv.util.TvProviderUriMatcher;
+import com.android.tv.util.TvUriMatcher;
import java.util.ArrayList;
import java.util.Collection;
@@ -505,8 +505,8 @@ public class RecommendationDataManager implements WatchedHistoryManager.Listener
@SuppressLint("SwitchIntDef")
@Override
public void onChange(final boolean selfChange, final Uri uri) {
- switch (TvProviderUriMatcher.match(uri)) {
- case TvProviderUriMatcher.MATCH_WATCHED_PROGRAM_ID:
+ switch (TvUriMatcher.match(uri)) {
+ case TvUriMatcher.MATCH_WATCHED_PROGRAM_ID:
if (!mHandler.hasMessages(MSG_UPDATE_WATCH_HISTORY,
TvContract.WatchedPrograms.CONTENT_URI)) {
mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, uri).sendToTarget();
diff --git a/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java
new file mode 100644
index 00000000..ad55afb7
--- /dev/null
+++ b/src/com/android/tv/recommendation/RecordedProgramPreviewUpdater.java
@@ -0,0 +1,176 @@
+/*
+ * 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.recommendation;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.TvApplication;
+import com.android.tv.data.PreviewDataManager;
+import com.android.tv.data.PreviewProgramContent;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.data.RecordedProgram;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Class to update the preview data for {@link RecordedProgram}
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+public class RecordedProgramPreviewUpdater {
+ private static final String TAG = "RecordedProgramPreviewUpdater";
+ // STOPSHIP: set it to false.
+ private static final boolean DEBUG = true;
+
+ private static final int RECOMMENDATION_COUNT = 6;
+
+ private static RecordedProgramPreviewUpdater sRecordedProgramPreviewUpdater;
+
+ /**
+ * Creates and returns the {@link RecordedProgramPreviewUpdater}.
+ */
+ public static RecordedProgramPreviewUpdater getInstance(Context context) {
+ if (sRecordedProgramPreviewUpdater == null) {
+ sRecordedProgramPreviewUpdater
+ = new RecordedProgramPreviewUpdater(context.getApplicationContext());
+ }
+ return sRecordedProgramPreviewUpdater;
+ }
+
+ private final Context mContext;
+ private final PreviewDataManager mPreviewDataManager;
+ private final DvrDataManager mDvrDataManager;
+
+ private RecordedProgramPreviewUpdater(Context context) {
+ mContext = context.getApplicationContext();
+ ApplicationSingletons applicationSingletons = TvApplication.getSingletons(mContext);
+ mPreviewDataManager = applicationSingletons.getPreviewDataManager();
+ mDvrDataManager = applicationSingletons.getDvrDataManager();
+ mDvrDataManager.addRecordedProgramListener(new DvrDataManager.RecordedProgramListener() {
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ if (DEBUG) Log.d(TAG, "Add new preview recorded programs");
+ updatePreviewDataForRecordedPrograms();
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ if (DEBUG) Log.d(TAG, "Update preview recorded programs");
+ updatePreviewDataForRecordedPrograms();
+ }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ if (DEBUG) Log.d(TAG, "Delete preview recorded programs");
+ updatePreviewDataForRecordedPrograms();
+ }
+ });
+ }
+
+ /**
+ * Updates the preview data for recorded programs.
+ */
+ public void updatePreviewDataForRecordedPrograms() {
+ if (!mPreviewDataManager.isLoadFinished()) {
+ mPreviewDataManager.addListener(new PreviewDataManager.PreviewDataListener() {
+ @Override
+ public void onPreviewDataLoadFinished() {
+ mPreviewDataManager.removeListener(this);
+ updatePreviewDataForRecordedPrograms();
+ }
+
+ @Override
+ public void onPreviewDataUpdateFinished() { }
+ });
+ return;
+ }
+ if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
+ mDvrDataManager.addRecordedProgramLoadFinishedListener(
+ new DvrDataManager.OnRecordedProgramLoadFinishedListener() {
+ @Override
+ public void onRecordedProgramLoadFinished() {
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ updatePreviewDataForRecordedPrograms();
+ }
+ });
+ return;
+ }
+ updatePreviewDataForRecordedProgramsInternal();
+ }
+
+ private void updatePreviewDataForRecordedProgramsInternal() {
+ Set<RecordedProgram> recordedPrograms = generateRecommendationRecordedPrograms();
+ Long recordedPreviewChannelId = mPreviewDataManager.getPreviewChannelId(
+ PreviewDataManager.TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL);
+ if (recordedPreviewChannelId == PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID
+ && !recordedPrograms.isEmpty()) {
+ createPreviewChannelForRecordedPrograms();
+ } else {
+ mPreviewDataManager.updatePreviewProgramsForChannel(recordedPreviewChannelId,
+ generatePreviewProgramContentsFromRecordedPrograms(
+ recordedPreviewChannelId, recordedPrograms), null);
+ }
+ }
+
+ private void createPreviewChannelForRecordedPrograms() {
+ mPreviewDataManager.createPreviewChannel(
+ PreviewDataManager.TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL,
+ new PreviewDataManager.OnPreviewChannelCreationResultListener() {
+ @Override
+ public void onPreviewChannelCreationResult(long createdPreviewChannelId) {
+ if (createdPreviewChannelId
+ != PreviewDataManager.INVALID_PREVIEW_CHANNEL_ID) {
+ updatePreviewDataForRecordedProgramsInternal();
+ }
+ }
+ });
+ }
+
+ private Set<RecordedProgram> generateRecommendationRecordedPrograms() {
+ Set<RecordedProgram> programs = new HashSet<>();
+ ArrayList<RecordedProgram> sortedRecordedPrograms =
+ new ArrayList<>(mDvrDataManager.getRecordedPrograms());
+ Collections.sort(
+ sortedRecordedPrograms, RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed());
+ for (RecordedProgram recordedProgram : sortedRecordedPrograms) {
+ if (!TextUtils.isEmpty(recordedProgram.getPosterArtUri())) {
+ programs.add(recordedProgram);
+ if (programs.size() >= RECOMMENDATION_COUNT) {
+ break;
+ }
+ }
+ }
+ return programs;
+ }
+
+ private Set<PreviewProgramContent> generatePreviewProgramContentsFromRecordedPrograms(
+ long previewChannelId, Set<RecordedProgram> recordedPrograms) {
+ Set<PreviewProgramContent> result = new HashSet<>();
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ result.add(PreviewProgramContent.createFromRecordedProgram(mContext, previewChannelId,
+ recordedProgram));
+ }
+ return result;
+ }
+}
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/LocalSearchProvider.java b/src/com/android/tv/search/LocalSearchProvider.java
index 9255a43d..ef9336d7 100644
--- a/src/com/android/tv/search/LocalSearchProvider.java
+++ b/src/com/android/tv/search/LocalSearchProvider.java
@@ -23,10 +23,19 @@ import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.TvApplication;
+import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
+import com.android.tv.perf.EventNames;
+import com.android.tv.perf.PerformanceMonitor;
+import com.android.tv.perf.TimerEvent;
import com.android.tv.util.PermissionUtils;
+import com.android.tv.util.TvUriMatcher;
import java.util.ArrayList;
import java.util.Arrays;
@@ -36,6 +45,9 @@ public class LocalSearchProvider extends ContentProvider {
private static final String TAG = "LocalSearchProvider";
private static final boolean DEBUG = false;
+ /** The authority for LocalSearchProvider. */
+ public static final String AUTHORITY = "com.android.tv.search";
+
public static final int PROGRESS_PERCENTAGE_HIDE = -1;
// TODO: Remove this once added to the SearchManager.
@@ -56,58 +68,93 @@ public class LocalSearchProvider extends ContentProvider {
};
private static final String EXPECTED_PATH_PREFIX = "/" + SearchManager.SUGGEST_URI_PATH_QUERY;
+ static final String SUGGEST_PARAMETER_ACTION = "action";
// The launcher passes 10 as a 'limit' parameter by default.
- private static final int DEFAULT_SEARCH_LIMIT = 10;
+ @VisibleForTesting
+ static final int DEFAULT_SEARCH_LIMIT = 10;
+ @VisibleForTesting
+ static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
private static final String NO_LIVE_CONTENTS = "0";
private static final String LIVE_CONTENTS = "1";
- static final String SUGGEST_PARAMETER_ACTION = "action";
- static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
+ private PerformanceMonitor mPerformanceMonitor;
+
+ /** Used only for testing */
+ private SearchInterface mSearchInterface;
@Override
public boolean onCreate() {
+ mPerformanceMonitor = TvApplication.getSingletons(getContext()).getPerformanceMonitor();
return true;
}
+ @VisibleForTesting
+ void setSearchInterface(SearchInterface searchInterface) {
+ SoftPreconditions.checkState(TvCommonUtils.isRunningInTest());
+ mSearchInterface = searchInterface;
+ }
+
@Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
+ public Cursor query(@NonNull Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (TvUriMatcher.match(uri) != TvUriMatcher.MATCH_ON_DEVICE_SEARCH) {
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+ TimerEvent queryTimer = mPerformanceMonitor.startTimer();
if (DEBUG) {
Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", "
+ Arrays.toString(selectionArgs) + ", " + sortOrder + ")");
}
long time = SystemClock.elapsedRealtime();
- SearchInterface search;
- if (PermissionUtils.hasAccessAllEpg(getContext())) {
- if (DEBUG) Log.d(TAG, "Performing TV Provider search.");
- search = new TvProviderSearch(getContext());
- } else {
- if (DEBUG) Log.d(TAG, "Performing Data Manager search.");
- search = new DataManagerSearch(getContext());
+ SearchInterface search = mSearchInterface;
+ if (search == null) {
+ if (PermissionUtils.hasAccessAllEpg(getContext())) {
+ if (DEBUG) Log.d(TAG, "Performing TV Provider search.");
+ search = new TvProviderSearch(getContext());
+ } else {
+ if (DEBUG) Log.d(TAG, "Performing Data Manager search.");
+ search = new DataManagerSearch(getContext());
+ }
}
String query = uri.getLastPathSegment();
- int limit = DEFAULT_SEARCH_LIMIT;
- int action = DEFAULT_SEARCH_ACTION;
- try {
- limit = Integer.parseInt(uri.getQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT));
- action = Integer.parseInt(uri.getQueryParameter(SUGGEST_PARAMETER_ACTION));
- } catch (NumberFormatException | UnsupportedOperationException e) {
- // Ignore the exceptions
+ int limit = getQueryParamater(uri, SearchManager.SUGGEST_PARAMETER_LIMIT,
+ DEFAULT_SEARCH_LIMIT);
+ if (limit <= 0) {
+ limit = DEFAULT_SEARCH_LIMIT;
+ }
+ int action = getQueryParamater(uri, SUGGEST_PARAMETER_ACTION, DEFAULT_SEARCH_ACTION);
+ if (action < SearchInterface.ACTION_TYPE_START
+ || action > SearchInterface.ACTION_TYPE_END) {
+ action = DEFAULT_SEARCH_ACTION;
}
List<SearchResult> results = new ArrayList<>();
if (!TextUtils.isEmpty(query)) {
results.addAll(search.search(query, limit, action));
}
Cursor c = createSuggestionsCursor(results);
- if (DEBUG) Log.d(TAG, "Elapsed time: " + (SystemClock.elapsedRealtime() - time) + "(msec)");
+ if (DEBUG) {
+ Log.d(TAG, "Elapsed time(count=" + c.getCount() + "): "
+ + (SystemClock.elapsedRealtime() - time) + "(msec)");
+ }
+ mPerformanceMonitor.stopTimer(queryTimer, EventNames.ON_DEVICE_SEARCH);
return c;
}
+ private int getQueryParamater(Uri uri, String key, int defaultValue) {
+ try {
+ return Integer.parseInt(uri.getQueryParameter(key));
+ } catch (NumberFormatException | UnsupportedOperationException e) {
+ // Ignore the exceptions
+ }
+ return defaultValue;
+ }
+
private Cursor createSuggestionsCursor(List<SearchResult> results) {
MatrixCursor cursor = new MatrixCursor(SEARCHABLE_COLUMNS, results.size());
List<String> row = new ArrayList<>(SEARCHABLE_COLUMNS.length);
+ int index = 0;
for (SearchResult result : results) {
row.clear();
row.add(result.title);
@@ -122,6 +169,7 @@ public class LocalSearchProvider extends ContentProvider {
row.add(result.duration == 0 ? null : String.valueOf(result.duration));
row.add(String.valueOf(result.progressPercentage));
cursor.addRow(row);
+ if (DEBUG) Log.d(TAG, "Result[" + (++index) + "]: " + result);
}
return cursor;
}
@@ -171,9 +219,20 @@ public class LocalSearchProvider extends ContentProvider {
@Override
public String toString() {
- return "channelId: " + channelId +
- ", channelNumber: " + channelNumber +
- ", title: " + title;
+ return "SearchResult{channelId=" + channelId +
+ ", channelNumber=" + channelNumber +
+ ", title=" + title +
+ ", description=" + description +
+ ", imageUri=" + imageUri +
+ ", intentAction=" + intentAction +
+ ", intentData=" + intentData +
+ ", contentType=" + contentType +
+ ", isLive=" + isLive +
+ ", videoWidth=" + videoWidth +
+ ", videoHeight=" + videoHeight +
+ ", duration=" + duration +
+ ", progressPercentage=" + progressPercentage +
+ "}";
}
}
} \ No newline at end of file
diff --git a/src/com/android/tv/search/SearchInterface.java b/src/com/android/tv/search/SearchInterface.java
index caa45812..d631972a 100644
--- a/src/com/android/tv/search/SearchInterface.java
+++ b/src/com/android/tv/search/SearchInterface.java
@@ -24,11 +24,11 @@ import java.util.List;
* Interface for channel and program search.
*/
public interface SearchInterface {
- String SOURCE_TV_SEARCH = "TvSearch";
-
+ int ACTION_TYPE_START = 1;
int ACTION_TYPE_AMBIGUOUS = 1;
int ACTION_TYPE_SWITCH_CHANNEL = 2;
int ACTION_TYPE_SWITCH_INPUT = 3;
+ int ACTION_TYPE_END = 3;
/**
* Search channels, inputs, or programs.
diff --git a/src/com/android/tv/search/TvProviderSearch.java b/src/com/android/tv/search/TvProviderSearch.java
index 2ceec19a..e7d8a02d 100644
--- a/src/com/android/tv/search/TvProviderSearch.java
+++ b/src/com/android/tv/search/TvProviderSearch.java
@@ -402,9 +402,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) {
@@ -432,6 +430,9 @@ public class TvProviderSearch implements SearchInterface {
// Find exact matches first.
for (TvInputInfo input : inputList) {
+ if (input.getType() == TvInputInfo.TYPE_TUNER) {
+ continue;
+ }
String label = canonicalizeLabel(input.loadLabel(mContext));
String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
if (TextUtils.equals(query, label) || TextUtils.equals(query, customLabel)) {
@@ -449,6 +450,9 @@ public class TvProviderSearch implements SearchInterface {
// Then look for partial matches.
for (TvInputInfo input : inputList) {
+ if (input.getType() == TvInputInfo.TYPE_TUNER) {
+ continue;
+ }
String label = canonicalizeLabel(input.loadLabel(mContext));
String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
if ((label != null && label.contains(query)) ||
diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java
index 2dd36074..a255de3e 100644
--- a/src/com/android/tv/tuner/ChannelScanFileParser.java
+++ b/src/com/android/tv/tuner/ChannelScanFileParser.java
@@ -90,10 +90,6 @@ public class ChannelScanFileParser {
if (tokens.length != 3 && tokens.length != 4) {
continue;
}
- if (!tokens[0].equals("A")) {
- // Only support ATSC
- continue;
- }
scanChannelList.add(ScanChannel.forTuner(Integer.parseInt(tokens[1]), tokens[2],
tokens.length == 4 ? Integer.parseInt(tokens[3]) : null));
}
diff --git a/src/com/android/tv/tuner/UsbTunerHal.java b/src/com/android/tv/tuner/DvbTunerHal.java
index 22e35ea1..ea977230 100644
--- a/src/com/android/tv/tuner/UsbTunerHal.java
+++ b/src/com/android/tv/tuner/DvbTunerHal.java
@@ -26,9 +26,9 @@ import java.util.SortedSet;
import java.util.TreeSet;
/**
- * A class to handle a hardware USB tuner device.
+ * A class to handle a hardware Linux DVB API supported tuner device.
*/
-public class UsbTunerHal extends TunerHal {
+public class DvbTunerHal extends TunerHal {
private static final Object sLock = new Object();
// @GuardedBy("sLock")
@@ -37,7 +37,7 @@ public class UsbTunerHal extends TunerHal {
private final DvbDeviceAccessor mDvbDeviceAccessor;
private DvbDeviceInfoWrapper mDvbDeviceInfo;
- protected UsbTunerHal(Context context) {
+ protected DvbTunerHal(Context context) {
super(context);
mDvbDeviceAccessor = new DvbDeviceAccessor(context);
}
@@ -55,6 +55,7 @@ public class UsbTunerHal extends TunerHal {
if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo);
mDvbDeviceInfo = deviceInfo;
sUsedDvbDevices.add(deviceInfo);
+ getDeliverySystemTypeFromDevice();
return true;
}
}
@@ -169,6 +170,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/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java
index de19766e..1176cdf0 100644
--- a/src/com/android/tv/tuner/TunerHal.java
+++ b/src/com/android/tv/tuner/TunerHal.java
@@ -19,7 +19,12 @@ package com.android.tv.tuner;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.StringDef;
+import android.support.annotation.WorkerThread;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.Features;
+import com.android.tv.customization.TvCustomizationManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -46,14 +51,43 @@ public abstract class TunerHal implements AutoCloseable {
public static final String MODULATION_8VSB = "8VSB";
public static final String MODULATION_QAM256 = "QAM256";
+ @IntDef({ DELIVERY_SYSTEM_UNDEFINED, DELIVERY_SYSTEM_ATSC, DELIVERY_SYSTEM_DVBC,
+ DELIVERY_SYSTEM_DVBS, DELIVERY_SYSTEM_DVBS2, DELIVERY_SYSTEM_DVBT,
+ DELIVERY_SYSTEM_DVBT2 })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeliverySystemType {}
+ public static final int DELIVERY_SYSTEM_UNDEFINED = 0;
+ public static final int DELIVERY_SYSTEM_ATSC = 1;
+ public static final int DELIVERY_SYSTEM_DVBC = 2;
+ public static final int DELIVERY_SYSTEM_DVBS = 3;
+ public static final int DELIVERY_SYSTEM_DVBS2 = 4;
+ public static final int DELIVERY_SYSTEM_DVBT = 5;
+ public static final int DELIVERY_SYSTEM_DVBT2 = 6;
+
+ @IntDef({ TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TunerType {}
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;
+ protected static final int PID_DVB_SDT = 0x0011;
+ protected static final int PID_DVB_EIT = 0x0012;
protected static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000;
protected static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for
// QAM256 tuning.
+ @IntDef({
+ BUILT_IN_TUNER_TYPE_LINUX_DVB
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface BuiltInTunerType {}
+ private static final int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1;
+
+ private static Integer sBuiltInTunerType;
+
+ protected @DeliverySystemType int mDeliverySystemType;
private boolean mIsStreaming;
private int mFrequency;
private String mModulation;
@@ -67,33 +101,62 @@ public abstract class TunerHal implements AutoCloseable {
* @param context context for creating the TunerHal instance
* @return the TunerHal instance
*/
+ @WorkerThread
public synchronized static TunerHal createInstance(Context context) {
TunerHal tunerHal = null;
- if (getTunerType(context) == TUNER_TYPE_BUILT_IN) {
- }
- if (tunerHal == null) {
- tunerHal = new UsbTunerHal(context);
- }
- if (tunerHal.openFirstAvailable()) {
- return tunerHal;
+ if (DvbTunerHal.getNumberOfDevices(context) > 0) {
+ if (DEBUG) Log.d(TAG, "Use DvbTunerHal");
+ tunerHal = new DvbTunerHal(context);
}
- 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) {
+ @WorkerThread
+ public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+ if (useBuiltInTuner(context)) {
+ if (getBuiltInTunerType(context) == BUILT_IN_TUNER_TYPE_LINUX_DVB) {
+ return new Pair<>(TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context));
+ }
+ } else {
+ int usbTunerCount = DvbTunerHal.getNumberOfDevices(context);
+ if (usbTunerCount > 0) {
+ return new Pair<>(TUNER_TYPE_USB, usbTunerCount);
+ }
}
- return UsbTunerHal.getNumberOfDevices(context);
+ return new Pair<>(null, 0);
+ }
+
+ /**
+ * Check a delivery system is for DVB or not.
+ */
+ public static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) {
+ return deliverySystemType == DELIVERY_SYSTEM_DVBC
+ || deliverySystemType == DELIVERY_SYSTEM_DVBS
+ || deliverySystemType == DELIVERY_SYSTEM_DVBS2
+ || deliverySystemType == DELIVERY_SYSTEM_DVBT
+ || deliverySystemType == DELIVERY_SYSTEM_DVBT2;
}
/**
- * 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 getBuiltInTunerType(context) != 0;
+ }
+
+ private static @BuiltInTunerType int getBuiltInTunerType(Context context) {
+ if (sBuiltInTunerType == null) {
+ sBuiltInTunerType = 0;
+ if (TvCustomizationManager.hasLinuxDvbBuiltInTuner(context)
+ && DvbTunerHal.getNumberOfDevices(context) > 0) {
+ sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB;
+ }
+ }
+ return sBuiltInTunerType;
}
protected TunerHal(Context context) {
@@ -106,6 +169,20 @@ public abstract class TunerHal implements AutoCloseable {
return mIsStreaming;
}
+ protected void getDeliverySystemTypeFromDevice() {
+ if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) {
+ mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId());
+ }
+ }
+
+ /**
+ * 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 +208,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;
@@ -148,6 +228,10 @@ public abstract class TunerHal implements AutoCloseable {
if (mFrequency == frequency && Objects.equals(mModulation, modulation)) {
addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
+ if (isDvbDeliverySystem(mDeliverySystemType)) {
+ addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
+ addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
+ }
mIsStreaming = true;
return true;
}
@@ -156,6 +240,10 @@ public abstract class TunerHal implements AutoCloseable {
if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) {
addPidFilter(PID_PAT, FILTER_TYPE_OTHER);
addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER);
+ if (isDvbDeliverySystem(mDeliverySystemType)) {
+ addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER);
+ addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER);
+ }
mFrequency = frequency;
mModulation = modulation;
mIsStreaming = true;
@@ -189,6 +277,7 @@ public abstract class TunerHal implements AutoCloseable {
protected native void nativeAddPidFilter(long deviceId, int pid, @FilterType int filterType);
protected native void nativeCloseAllPidFilters(long deviceId);
protected native void nativeSetHasPendingTune(long deviceId, boolean hasPendingTune);
+ protected native int nativeGetDeliverySystemType(long deviceId);
/**
* Stops current tuning. The tuner device and pid filters will be reset by this call and make
@@ -210,6 +299,10 @@ public abstract class TunerHal implements AutoCloseable {
nativeSetHasPendingTune(getDeviceId(), hasPendingTune);
}
+ public int getDeliverySystemType() {
+ return mDeliverySystemType;
+ }
+
protected native void nativeStopTune(long deviceId);
/**
@@ -235,7 +328,7 @@ public abstract class TunerHal implements AutoCloseable {
/**
* Opens Linux DVB frontend device. This method is called from native JNI and used only for
- * UsbTunerHal.
+ * DvbTunerHal.
*/
protected int openDvbFrontEndFd() {
return -1;
@@ -243,7 +336,7 @@ public abstract class TunerHal implements AutoCloseable {
/**
* Opens Linux DVB demux device. This method is called from native JNI and used only for
- * UsbTunerHal.
+ * DvbTunerHal.
*/
protected int openDvbDemuxFd() {
return -1;
@@ -251,7 +344,7 @@ public abstract class TunerHal implements AutoCloseable {
/**
* Opens Linux DVB dvr device. This method is called from native JNI and used only for
- * UsbTunerHal.
+ * DvbTunerHal.
*/
protected int openDvbDvrFd() {
return -1;
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
index d89b6a0c..e06b9b4a 100644
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ b/src/com/android/tv/tuner/TunerInputController.java
@@ -16,30 +16,43 @@
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.support.annotation.NonNull;
+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.common.SoftPreconditions;
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.Set;
+import java.util.concurrent.TimeUnit;
/**
* Controls the package visibility of {@link TunerTvInputService}.
@@ -48,84 +61,94 @@ import java.util.Map;
* {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}
* to update the connection status of the supported USB TV tuners.
*/
-public class TunerInputController extends BroadcastReceiver {
+public class TunerInputController {
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.
+ */
+ private static final String CHECKING_NETWORK_CONNECTION =
+ "com.android.tv.action.CHECKING_NETWORK_CONNECTION";
+
+ 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)
+ // STOPSHIP: Add WinTV-soloHD (Isoc) temporary for test. Remove this after test complete.
+ new TunerDevice(0x2040, 0x0264, null),
};
private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
private static final long DVB_DRIVER_CHECK_DELAY_MS = 300;
- private DvbDeviceAccessor mDvbDeviceAccessor;
- private final Handler mHandler = new Handler(Looper.getMainLooper()) {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case MSG_ENABLE_INPUT_SERVICE:
- Context context = (Context) msg.obj;
- if (mDvbDeviceAccessor == null) {
- mDvbDeviceAccessor = new DvbDeviceAccessor(context);
- }
- enableTunerTvInputService(context, mDvbDeviceAccessor.isDvbDeviceAvailable());
- break;
- }
- }
- };
-
/**
- * Simple data holder for a USB device. Used to represent a tuner model, and compare
- * against {@link UsbDevice}.
+ * Checks status of USB devices to see if there are available USB tuners connected.
*/
- private static class TunerDevice {
- private final int vendorId;
- private final int productId;
-
- private TunerDevice(int vendorId, int productId) {
- this.vendorId = vendorId;
- this.productId = productId;
- }
-
- private boolean equals(UsbDevice device) {
- return device.getVendorId() == vendorId && device.getProductId() == productId;
- }
+ public static void onCheckingUsbTunerStatus(Context context, String action) {
+ onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler());
}
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
- TvApplication.setCurrentRunningProcess(context, true);
- if (!Features.TUNER.isEnabled(context)) {
- enableTunerTvInputService(context, false);
+ private static void onCheckingUsbTunerStatus(Context context, String action,
+ @NonNull CheckDvbDeviceHandler handler) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ if (TunerHal.useBuiltInTuner(context)) {
+ enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN);
return;
}
+ // Falls back to the below to check USB tuner devices.
+ boolean enabled = isUsbTunerConnected(context);
+ handler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
+ if (enabled) {
+ // Need to check if DVB driver is accessible. Since the driver creation
+ // could be happen after the USB event, delay the checking by
+ // DVB_DRIVER_CHECK_DELAY_MS.
+ handler.sendMessageDelayed(handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
+ DVB_DRIVER_CHECK_DELAY_MS);
+ } else {
+ 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);
+ return;
+ }
+ enableTunerTvInputService(context, false, false, TextUtils
+ .equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) ?
+ TunerHal.TUNER_TYPE_USB : null);
+ }
+ }
- switch (intent.getAction()) {
- case Intent.ACTION_BOOT_COMPLETED:
- 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);
- break;
- }
- // Falls back to the below to check USB tuner devices.
- boolean enabled = isUsbTunerConnected(context);
- mHandler.removeMessages(MSG_ENABLE_INPUT_SERVICE);
- if (enabled) {
- // Need to check if DVB driver is accessible. Since the driver creation
- // could be happen after the USB event, delay the checking by
- // DVB_DRIVER_CHECK_DELAY_MS.
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
- DVB_DRIVER_CHECK_DELAY_MS);
- } else {
- enableTunerTvInputService(context, false);
- }
- break;
+ private static void onNetworkTunerChanged(Context context, boolean enabled) {
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ if (enabled) {
+ // 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);
+ } else {
+ 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);
+ }
}
}
@@ -135,15 +158,18 @@ public class TunerInputController extends BroadcastReceiver {
* @param context {@link Context} instance
* @return {@code true} if any tuner device we support is plugged in
*/
- private boolean isUsbTunerConnected(Context context) {
+ private static boolean isUsbTunerConnected(Context context) {
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
Map<String, UsbDevice> deviceList = manager.getDeviceList();
+ String currentSecurityLevel =
+ SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
+
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equals(device)) {
+ if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
return true;
}
@@ -158,7 +184,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 static 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,23 +197,182 @@ 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;
if (newState != pm.getComponentEnabledSetting(componentName)) {
- // Send/cancel the USB tuner TV input setup recommendation card.
- TunerSetupActivity.onTvInputEnabled(context, enabled);
+ // Send/cancel the USB tuner TV input setup notification.
+ TunerSetupActivity.onTvInputEnabled(context, enabled, tunerType);
// 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) {
+ boolean runningInMainProcess =
+ TvApplication.getSingletons(context).isRunningInMainProcess();
+ SoftPreconditions.checkState(runningInMainProcess);
+ if (!runningInMainProcess) {
+ return;
+ }
+ executeNetworkTunerDiscoveryAsyncTask(context, 0);
+ }
+
+ /**
+ * Discovers a network tuner.
+ * @param context {@link Context}
+ * @param repeatedDurationMs the time length to wait to repeatedly check network status to start
+ * finding network tuner when the network connection is not available.
+ * {@code 0} to disable repeatedly checking.
+ */
+ private static void executeNetworkTunerDiscoveryAsyncTask(final Context context,
+ final long repeatedDurationMs) {
+ if (!Features.NETWORK_TUNER.isEnabled(context)) {
+ return;
+ }
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ protected Boolean 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, IntentReceiver.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;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result == null) {
+ return;
+ }
+ onNetworkTunerChanged(context, result);
+ }
+ }.execute();
+ }
+
+ private static boolean isNetworkConnected(Context context) {
+ ConnectivityManager cm = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
+
+ public static class IntentReceiver extends BroadcastReceiver {
+ private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
+ TvApplication.setCurrentRunningProcess(context, true);
+ if (!Features.TUNER.isEnabled(context)) {
+ enableTunerTvInputService(context, false, false, null);
+ return;
+ }
+ 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:
+ onCheckingUsbTunerStatus(context, intent.getAction(), mHandler);
+ 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;
+ }
+ }
+ }
+
+ /**
+ * Simple data holder for a USB device. Used to represent a tuner model, and compare
+ * against {@link UsbDevice}.
+ */
+ private static class TunerDevice {
+ private final int vendorId;
+ private final 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;
+ }
+ }
+
+ private static class CheckDvbDeviceHandler extends Handler {
+ private DvbDeviceAccessor mDvbDeviceAccessor;
+
+ CheckDvbDeviceHandler() {
+ super(Looper.getMainLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ENABLE_INPUT_SERVICE:
+ Context context = (Context) msg.obj;
+ if (mDvbDeviceAccessor == null) {
+ mDvbDeviceAccessor = new DvbDeviceAccessor(context);
+ }
+ boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
+ enableTunerTvInputService(
+ context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null);
+ break;
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java
index 1547e3ae..11a6a969 100644
--- a/src/com/android/tv/tuner/TunerPreferences.java
+++ b/src/com/android/tv/tuner/TunerPreferences.java
@@ -25,11 +25,15 @@ import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
+import android.support.annotation.GuardedBy;
+import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.TunerPreferenceProvider.Preferences;
import com.android.tv.tuner.util.TisConfiguration;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
/**
* A helper class for the USB tuner preferences.
@@ -39,21 +43,53 @@ 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";
+ private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting";
+ private static final String PREFS_KEY_TRICKPLAY_EXPIRED_MS = "trickplay_expired_ms";
private static final String SHARED_PREFS_NAME = "com.android.tv.tuner.preferences";
public static final int CHANNEL_DATA_VERSION_NOT_SET = -1;
+ @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TrickplaySetting {
+ }
+
+ /**
+ * Trickplay setting is not changed by a user. Trickplay will be enabled in this case.
+ */
+ public static final int TRICKPLAY_SETTING_NOT_SET = -1;
+
+ /**
+ * Trickplay setting is disabled.
+ */
+ public static final int TRICKPLAY_SETTING_DISABLED = 0;
+
+ /**
+ * Trickplay setting is enabled.
+ */
+ public static final int TRICKPLAY_SETTING_ENABLED = 1;
+
+ @GuardedBy("TunerPreferences.class")
private static final Bundle sPreferenceValues = new Bundle();
private static LoadPreferencesTask sLoadPreferencesTask;
private static ContentObserver sContentObserver;
+ private static TunerPreferencesChangedListener sPreferencesChangedListener = null;
private static boolean sInitialized;
/**
+ * Listeners for TunerPreferences change.
+ */
+ public interface TunerPreferencesChangedListener {
+ void onTunerPreferencesChanged();
+ }
+
+ /**
* Initializes the USB tuner preferences.
*/
@MainThread
@@ -86,11 +122,19 @@ 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);
}
+ setTunerPreferencesChangedListener(null);
+ }
+
+ /**
+ * Sets the listener for TunerPreferences change.
+ */
+ public static void setTunerPreferencesChangedListener(
+ TunerPreferencesChangedListener listener) {
+ sPreferencesChangedListener = listener;
}
/**
@@ -99,7 +143,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 +158,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 +170,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 +180,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 +190,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 +200,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 +228,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 +238,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 +248,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 +258,50 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean getStoreTsStream(Context context) {
+ public static synchronized long getTrickplayExpiredMs(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getLong(PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0);
+ } else {
+ return getSharedPreferences(context)
+ .getLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0);
+ }
+ }
+
+ public static synchronized void setTrickplayExpiredMs(Context context, long timeMs) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs);
+ } else {
+ getSharedPreferences(context).edit()
+ .putLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs)
+ .apply();
+ }
+ }
+
+ public static synchronized @TrickplaySetting int getTrickplaySetting(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
+ } else {
+ return getSharedPreferences(context)
+ .getInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
+ }
+ }
+
+ public static synchronized void setTrickplaySetting(Context context,
+ @TrickplaySetting int trickplaySetting) {
+ SoftPreconditions.checkState(sInitialized);
+ SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET);
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting);
+ } else {
+ getSharedPreferences(context).edit()
+ .putInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting)
+ .apply();
+ }
+ }
+
+ public static synchronized boolean getStoreTsStream(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
@@ -214,8 +311,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 +325,28 @@ 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, long value) {
+ sPreferenceValues.putLong(key, value);
+ savePreference(context, key, Long.toString(value));
+ }
+
+ private static synchronized void setPreference(Context context, String key, boolean value) {
+ sPreferenceValues.putBoolean(key, value);
+ savePreference(context, key, Boolean.toString(value));
+ }
+
+ private static void savePreference(final Context context, final String key,
+ final String value) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -249,18 +365,6 @@ public class TunerPreferences {
}.execute();
}
- @MainThread
- private static void setPreference(Context context, String key, int value) {
- sPreferenceValues.putInt(key, value);
- setPreference(context, key, Integer.toString(value));
- }
-
- @MainThread
- private static void setPreference(Context context, String key, boolean value) {
- sPreferenceValues.putBoolean(key, value);
- setPreference(context, key, Boolean.toString(value));
- }
-
private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
private final Context mContext;
private LoadPreferencesTask(Context context) {
@@ -279,8 +383,12 @@ public class TunerPreferences {
String key = cursor.getString(0);
String value = cursor.getString(1);
switch (key) {
+ case PREFS_KEY_TRICKPLAY_EXPIRED_MS:
+ bundle.putLong(key, Long.parseLong(value));
+ break;
case PREFS_KEY_CHANNEL_DATA_VERSION:
case PREFS_KEY_SCANNED_CHANNEL_COUNT:
+ case PREFS_KEY_TRICKPLAY_SETTING:
try {
bundle.putInt(key, Integer.parseInt(value));
} catch (NumberFormatException e) {
@@ -292,6 +400,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;
}
}
}
@@ -304,7 +415,14 @@ public class TunerPreferences {
@Override
protected void onPostExecute(Bundle bundle) {
- sPreferenceValues.putAll(bundle);
+ synchronized (TunerPreferences.class) {
+ if (bundle != null) {
+ sPreferenceValues.putAll(bundle);
+ }
+ }
+ if (sPreferencesChangedListener != null) {
+ sPreferencesChangedListener.onTunerPreferencesChanged();
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 3c75caa9..24a0f354 100644
--- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -245,6 +245,10 @@ public class CaptionTrackRenderer implements Handler.Callback {
}
}
+ public void clear() {
+ mHandler.sendEmptyMessage(MSG_CAPTION_CLEAR);
+ }
+
public void reset() {
mCurrentWindowLayout = null;
mIsDelayed = false;
diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java
index 92ab0620..d0f6cf11 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() {
@@ -208,6 +209,15 @@ public class Cea708Parser {
}
}
+ public void clear() {
+ mDtvCcPacket.clear();
+ mCcPackets.clear();
+ mBuffer.setLength(0);
+ mDiscoveredNumBytes.clear();
+ mCommand = 0;
+ mDtvCcPacking = false;
+ }
+
public void setListenServiceNumber(int serviceNumber) {
mListenServiceNumber = serviceNumber;
}
@@ -332,12 +342,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/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java
index aead4be8..8f98e67c 100644
--- a/src/com/android/tv/tuner/data/PsipData.java
+++ b/src/com/android/tv/tuner/data/PsipData.java
@@ -24,9 +24,10 @@ import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
import com.android.tv.tuner.data.nano.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.HashMap;
import java.util.List;
import java.util.Locale;
@@ -226,6 +227,50 @@ public class PsipData {
}
}
+ public static class SdtItem {
+ private final String mServiceName;
+ private final String mServiceProviderName;
+ private final int mServiceType;
+ private final int mServiceId;
+ private final int mOriginalNetWorkId;
+
+ public SdtItem(String serviceName, String serviceProviderName, int serviceType,
+ int serviceId, int originalNetWorkId) {
+ mServiceName = serviceName;
+ mServiceProviderName = serviceProviderName;
+ mServiceType = serviceType;
+ mServiceId = serviceId;
+ mOriginalNetWorkId = originalNetWorkId;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ public String getServiceProviderName() {
+ return mServiceProviderName;
+ }
+
+ public int getServiceType() {
+ return mServiceType;
+ }
+
+ public int getServiceId() {
+ return mServiceId;
+ }
+
+ public int getOriginalNetworkId() {
+ return mOriginalNetWorkId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ServiceName: %s ServiceProviderName:%s ServiceType:%d "
+ + "OriginalNetworkId:%d",
+ mServiceName, mServiceProviderName, mServiceType, mOriginalNetWorkId);
+ }
+ }
+
/**
* A base class for descriptors of Ts packets.
*/
@@ -462,6 +507,92 @@ public class PsipData {
}
}
+ public static class ServiceDescriptor extends TsDescriptor {
+ private final int mServiceType;
+ private final String mServiceProviderName;
+ private final String mServiceName;
+
+ public ServiceDescriptor(int serviceType, String serviceProviderName, String serviceName) {
+ mServiceType = serviceType;
+ mServiceProviderName = serviceProviderName;
+ mServiceName = serviceName;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DVB_DESCRIPTOR_TAG_SERVICE;
+ }
+
+ public int getServiceType() {
+ return mServiceType;
+ }
+
+ public String getServiceProviderName() {
+ return mServiceProviderName;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Service descriptor, service type: %d, "
+ + "service provider name: %s, "
+ + "service name: %s", mServiceType, mServiceProviderName, mServiceName);
+ }
+ }
+
+ public static class ShortEventDescriptor extends TsDescriptor {
+ private final String mLanguage;
+ private final String mEventName;
+ private final String mText;
+
+ public ShortEventDescriptor(String language, String eventName, String text) {
+ mLanguage = language;
+ mEventName = eventName;
+ mText = text;
+ }
+
+ public String getEventName() {
+ return mEventName;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DVB_DESCRIPTOR_TAG_SHORT_EVENT;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ShortEvent Descriptor, language:%s, event name: %s, "
+ + "text:%s", mLanguage, mEventName, mText);
+ }
+ }
+
+ public static class ParentalRatingDescriptor extends TsDescriptor {
+ private final HashMap<String, Integer> mRatings;
+
+ public ParentalRatingDescriptor(HashMap<String, Integer> ratings) {
+ mRatings = ratings;
+ }
+
+ @Override
+ public int getTag() {
+ return SectionParser.DVB_DESCRIPTOR_TAG_PARENTAL_RATING;
+ }
+
+ public HashMap<String, Integer> getRatings() {
+ return mRatings;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Parental rating descriptor, ratings:" + mRatings);
+ }
+ }
+
public static class RatingRegion {
private final int mName;
private final String mDescription;
diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java
index 89079d77..1cf514c1 100644
--- a/src/com/android/tv/tuner/data/TunerChannel.java
+++ b/src/com/android/tv/tuner/data/TunerChannel.java
@@ -24,7 +24,7 @@ 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.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 +40,11 @@ import java.util.Objects;
public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracksInterface {
private static final String TAG = "TunerChannel";
+ /**
+ * Channel number separator between major number and minor number.
+ */
+ public static final char CHANNEL_NUMBER_SEPARATOR = '-';
+
// See ATSC Code Points Registry.
private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] {
"ATSC Reserved",
@@ -63,6 +68,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
// According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff.
public static final int INVALID_STREAMTYPE = -1;
+ // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766
private final TunerChannelProto mProto;
private TunerChannel(PsipData.VctItem channel, int programNumber,
@@ -88,6 +94,10 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
mProto.serviceType = channel.getServiceType();
}
+ initProto(pmtItems, type);
+ }
+
+ private void initProto(List<PsiData.PmtItem> pmtItems, int type) {
mProto.type = type;
mProto.channelId = -1L;
mProto.frequency = INVALID_FREQUENCY;
@@ -129,14 +139,44 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
mProto.audioTrackIndex = (audioPids.size() > 0) ? 0 : -1;
}
+ private TunerChannel(int programNumber, int type, PsipData.SdtItem channel,
+ List<PsiData.PmtItem> pmtItems) {
+ mProto = new TunerChannelProto();
+ mProto.tsid = 0;
+ mProto.virtualMajor = 0;
+ mProto.virtualMinor = 0;
+ if (channel == null) {
+ mProto.shortName = "";
+ mProto.programNumber = programNumber;
+ } else {
+ mProto.shortName = channel.getServiceName();
+ mProto.programNumber = channel.getServiceId();
+ mProto.serviceType = channel.getServiceType();
+ }
+ initProto(pmtItems, type);
+ }
+
+ /**
+ * Initialize tuner channel with VCT items and PMT items.
+ */
public TunerChannel(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) {
this(channel, 0, pmtItems, Channel.TYPE_TUNER);
}
+ /**
+ * Initialize tuner channel with program number and PMT items.
+ */
public TunerChannel(int programNumber, List<PsiData.PmtItem> pmtItems) {
this(null, programNumber, pmtItems, Channel.TYPE_TUNER);
}
+ /**
+ * Initialize tuner channel with SDT items and PMT items.
+ */
+ public TunerChannel(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) {
+ this(0, Channel.TYPE_TUNER, channel, pmtItems);
+ }
+
private TunerChannel(TunerChannelProto tunerChannelProto) {
mProto = tunerChannelProto;
}
@@ -145,6 +185,50 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE);
}
+ public static TunerChannel forDvbFile(
+ PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) {
+ return new TunerChannel(0, Channel.TYPE_FILE, channel, pmtItems);
+ }
+
+ /**
+ * 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(
+ null, programNumber, Collections.EMPTY_LIST, Channel.TYPE_NETWORK);
+ 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 +277,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.videoPid;
}
- public void setVideoPid(int videoPid) {
+ synchronized public void setVideoPid(int videoPid) {
mProto.videoPid = videoPid;
}
@@ -219,7 +303,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioPids);
}
- public void setAudioPids(List<Integer> audioPids) {
+ synchronized public void setAudioPids(List<Integer> audioPids) {
mProto.audioPids = Ints.toArray(audioPids);
}
@@ -227,7 +311,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioStreamTypes);
}
- public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
+ synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
}
@@ -239,32 +323,32 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.type;
}
- public void setFilepath(String filepath) {
- mProto.filepath = filepath;
+ synchronized public void setFilepath(String filepath) {
+ mProto.filepath = filepath == null ? "" : filepath;
}
public String getFilepath() {
return mProto.filepath;
}
- public void setVirtualMajor(int virtualMajor) {
+ synchronized public void setVirtualMajor(int virtualMajor) {
mProto.virtualMajor = virtualMajor;
}
- public void setVirtualMinor(int virtualMinor) {
+ synchronized public void setVirtualMinor(int virtualMinor) {
mProto.virtualMinor = virtualMinor;
}
- public void setShortName(String shortName) {
- mProto.shortName = shortName;
+ synchronized public void setShortName(String shortName) {
+ mProto.shortName = shortName == null ? "" : shortName;
}
- public void setFrequency(int frequency) {
+ synchronized public void setFrequency(int frequency) {
mProto.frequency = frequency;
}
- public void setModulation(String modulation) {
- mProto.modulation = modulation;
+ synchronized public void setModulation(String modulation) {
+ mProto.modulation = modulation == null ? "" : modulation;
}
public boolean hasVideo() {
@@ -279,13 +363,18 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.channelId;
}
- public void setChannelId(long channelId) {
+ synchronized public void setChannelId(long channelId) {
mProto.channelId = channelId;
}
public String getDisplayNumber() {
- if (mProto.virtualMajor != 0 && mProto.virtualMinor != 0) {
- return String.format("%d-%d", mProto.virtualMajor, mProto.virtualMinor);
+ return getDisplayNumber(true);
+ }
+
+ public String getDisplayNumber(boolean ignoreZeroMinorNumber) {
+ if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) {
+ return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR,
+ mProto.virtualMinor);
} else if (mProto.virtualMajor != 0) {
return Integer.toString(mProto.virtualMajor);
} else {
@@ -298,7 +387,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
@Override
- public void setHasCaptionTrack() {
+ synchronized public void setHasCaptionTrack() {
mProto.hasCaptionTrack = true;
}
@@ -312,7 +401,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks));
}
- public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
+ synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]);
}
@@ -321,11 +410,11 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks));
}
- public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]);
}
- public void selectAudioTrack(int index) {
+ synchronized public void selectAudioTrack(int index) {
if (0 <= index && index < mProto.audioPids.length) {
mProto.audioTrackIndex = index;
} else {
@@ -333,6 +422,22 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
}
+ synchronized public void setRecordingProhibited(boolean recordingProhibited) {
+ mProto.recordingProhibited = recordingProhibited;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mProto.recordingProhibited;
+ }
+
+ synchronized public void setVideoFormat(String videoFormat) {
+ mProto.videoFormat = videoFormat == null ? "" : videoFormat;
+ }
+
+ public String getVideoFormat() {
+ return mProto.videoFormat;
+ }
+
@Override
public String toString() {
switch (mProto.type) {
@@ -359,7 +464,10 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
if (ret != 0) {
return ret;
}
-
+ ret = StringUtils.compare(getName(), channel.getName());
+ if (ret != 0) {
+ return ret;
+ }
// For FileTsStreamer, file paths should be compared.
return StringUtils.compare(getFilepath(), channel.getFilepath());
}
@@ -374,12 +482,19 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
@Override
public int hashCode() {
- return Objects.hash(getFrequency(), getProgramNumber(), getFilepath());
+ return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath());
}
// Serialization
- public byte[] toByteArray() {
- return MessageNano.toByteArray(mProto);
+ synchronized public byte[] toByteArray() {
+ try {
+ return MessageNano.toByteArray(mProto);
+ } catch (Exception e) {
+ // Retry toByteArray. b/34197766
+ Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock",
+ e);
+ return MessageNano.toByteArray(mProto);
+ }
}
public static TunerChannel parseFrom(byte[] data) {
diff --git a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
index 5e839223..5f536708 100644
--- a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java
@@ -40,6 +40,7 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements
private static final boolean DEBUG = false;
public static final int MSG_SERVICE_NUMBER = 1;
+ public static final int MSG_ENABLE_CLOSED_CAPTION = 2;
// According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps.
private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8;
@@ -52,11 +53,13 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements
private long mCurrentPositionUs;
private long mPresentationTimeUs;
private int mTrackIndex;
+ private boolean mRenderingDisabled;
private Cea708Parser mCea708Parser;
private CcListener mCcListener;
public interface CcListener {
void emitEvent(CaptionEvent captionEvent);
+ void clearCaption();
void discoverServiceNumber(int serviceNumber);
}
@@ -204,7 +207,7 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements
}
case SampleSource.SAMPLE_READ: {
mSampleHolder.data.flip();
- if (mCea708Parser != null) {
+ if (mCea708Parser != null && !mRenderingDisabled) {
mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs);
}
return true;
@@ -274,10 +277,26 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
- if (messageType == MSG_SERVICE_NUMBER) {
- setServiceNumber((int) message);
- } else {
- super.handleMessage(messageType, message);
+ switch (messageType) {
+ case MSG_SERVICE_NUMBER:
+ setServiceNumber((int) message);
+ break;
+ case MSG_ENABLE_CLOSED_CAPTION:
+ boolean renderingDisabled = (Boolean) message == false;
+ if (mRenderingDisabled != renderingDisabled) {
+ mRenderingDisabled = renderingDisabled;
+ if (mRenderingDisabled) {
+ if (mCea708Parser != null) {
+ mCea708Parser.clear();
+ }
+ if (mCcListener != null) {
+ mCcListener.clearCaption();
+ }
+ }
+ }
+ break;
+ default:
+ super.handleMessage(messageType, message);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java
new file mode 100644
index 00000000..0ab6d8c4
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.exoplayer;
+
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.TimestampAdjuster;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Extractor factory, mainly aim at create TsExtractor with FLAG_ALLOW_NON_IDR_KEYFRAMES flags for
+ * H.264 stream
+ */
+public final class ExoPlayerExtractorsFactory implements ExtractorsFactory {
+ @Override
+ public Extractor[] createExtractors() {
+ // Only create TsExtractor since we only target MPEG2TS stream.
+ Extractor[] extractors = {
+ new TsExtractor(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory(
+ DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES), false) };
+ return extractors;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index c105e222..0b648400 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,17 +23,28 @@ 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.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.audio.MpegTsDefaultAudioTrackRenderer;
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 +53,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 +66,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 +78,69 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
private IOException mExceptionOnPrepare;
private List<MediaFormat> mTrackFormats;
+ private int mVideoTrackIndex = INVALID_TRACK_INDEX;
+ private boolean mVideoTrackMet;
+ private long mBaseSamplePts = Long.MIN_VALUE;
private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
+ private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>();
private OnCompletionListener mOnCompletionListener;
private Handler mOnCompletionListenerHandler;
private IOException mError;
- public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
+ public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager,
PlaybackBufferListener bufferListener, boolean isRecording) {
// It'll be used as a timeshift file chunk name's prefix.
mId = System.currentTimeMillis();
- Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
EventListener eventListener = new EventListener() {
-
@Override
- public void onLoadError(int sourceId, IOException e) {
- mError = e;
+ public void onLoadError(IOException error) {
+ mError = error;
}
};
mSourceReaderThread = new HandlerThread("SourceReaderThread");
- mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
- allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
+ mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri,
+ new com.google.android.exoplayer2.upstream.DataSource.Factory() {
+ @Override
+ public com.google.android.exoplayer2.upstream.DataSource createDataSource() {
+ // Returns an adapter implementation for ExoPlayer V2 DataSource interface.
+ return new com.google.android.exoplayer2.upstream.DataSource() {
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ return source.open(
+ new com.google.android.exoplayer.upstream.DataSpec(
+ dataSpec.uri, dataSpec.postBody,
+ dataSpec.absoluteStreamPosition, dataSpec.position,
+ dataSpec.length, dataSpec.key, dataSpec.flags));
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength)
+ throws IOException {
+ return source.read(buffer, offset, readLength);
+ }
+
+ @Override
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+ };
+ }
+ },
+ new ExoPlayerExtractorsFactory(),
// 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 +155,141 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mOnCompletionListenerHandler = handler;
}
- private class SourceReaderWorker implements Handler.Callback {
+ private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback {
public static final int MSG_PREPARE = 1;
public static final int MSG_FETCH_SAMPLES = 2;
public static final int MSG_RELEASE = 3;
private static final int RETRY_INTERVAL_MS = 50;
- private final SampleSource mSampleSource;
- private SampleSource.SampleSourceReader mSampleSourceReader;
+ private final MediaSource mSampleSource;
+ private MediaPeriod mMediaPeriod;
+ private SampleStream[] mStreams;
private boolean[] mTrackMetEos;
private boolean mMetEos = false;
private long mCurrentPosition;
+ private DecoderInputBuffer mDecoderInputBuffer;
+ private SampleHolder mSampleHolder;
+ private boolean mPrepareRequested;
- public SourceReaderWorker(SampleSource sampleSource) {
+ public SourceReaderWorker(MediaSource sampleSource) {
mSampleSource = sampleSource;
+ mSampleSource.prepareSource(null, false, new MediaSource.Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ // Dynamic stream change is not supported yet. b/28169263
+ // For now, this will cause EOS and playback reset.
+ }
+ });
+ mDecoderInputBuffer = new DecoderInputBuffer(
+ DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ MediaFormat convertFormat(Format format) {
+ if (format.sampleMimeType.startsWith("audio/")) {
+ return MediaFormat.createAudioFormat(format.id, format.sampleMimeType,
+ format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount,
+ format.sampleRate, format.initializationData, format.language,
+ format.pcmEncoding);
+ } else if (format.sampleMimeType.startsWith("video/")) {
+ return MediaFormat.createVideoFormat(
+ format.id, format.sampleMimeType, format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height,
+ format.initializationData, format.rotationDegrees,
+ format.pixelWidthHeightRatio, format.projectionData, format.stereoMode);
+ } else if (format.sampleMimeType.endsWith("/cea-608")
+ || format.sampleMimeType.startsWith("text/")) {
+ return MediaFormat.createTextFormat(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language);
+ } else {
+ return MediaFormat.createFormatForMimeType(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US);
+ }
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ if (mMediaPeriod == null) {
+ // This instance is already released while the extractor is preparing.
+ return;
+ }
+ TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory();
+ TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups();
+ TrackSelection[] selections = new TrackSelection[trackGroupArray.length];
+ for (int i = 0; i < selections.length; ++i) {
+ selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0);
+ }
+ boolean retain[] = new boolean[trackGroupArray.length];
+ boolean reset[] = new boolean[trackGroupArray.length];
+ mStreams = new SampleStream[trackGroupArray.length];
+ mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0);
+ if (mTrackFormats == null) {
+ int trackCount = trackGroupArray.length;
+ mTrackMetEos = new boolean[trackCount];
+ List<MediaFormat> trackFormats = new ArrayList<>();
+ int videoTrackCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ Format format = trackGroupArray.get(i).getFormat(0);
+ if (format.sampleMimeType.startsWith("video/")) {
+ videoTrackCount++;
+ mVideoTrackIndex = i;
+ }
+ trackFormats.add(convertFormat(format));
+ }
+ if (videoTrackCount > 1) {
+ // Disable dropping samples when there are multiple video tracks.
+ mVideoTrackIndex = INVALID_TRACK_INDEX;
+ }
+ mTrackFormats = trackFormats;
+ List<String> ids = new ArrayList<>();
+ for (int i = 0; i < mTrackFormats.size(); i++) {
+ ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
+ }
+ try {
+ mSampleBuffer.init(ids, mTrackFormats);
+ } catch (IOException e) {
+ // In this case, we will not schedule any further operation.
+ // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
+ // call release() eventually.
+ mExceptionOnPrepare = e;
+ return;
+ }
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ mPrepared = true;
+ }
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ source.continueLoading(mCurrentPosition);
}
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_PREPARE:
- mPrepared = prepare();
- if (!mPrepared && mExceptionOnPrepare == null) {
- mSourceReaderHandler
- .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS);
- } else{
- mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ if (!mPrepareRequested) {
+ mPrepareRequested = true;
+ mMediaPeriod = mSampleSource.createPeriod(0,
+ new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0);
+ mMediaPeriod.prepare(this);
+ try {
+ mMediaPeriod.maybeThrowPrepareError();
+ } catch (IOException e) {
+ mError = e;
+ }
}
return true;
case MSG_FETCH_SAMPLES:
boolean didSomething = false;
- SampleHolder sample = new SampleHolder(
- SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
ConditionVariable conditionVariable = new ConditionVariable();
- int trackCount = mSampleSourceReader.getTrackCount();
+ int trackCount = mStreams.length;
for (int i = 0; i < trackCount; ++i) {
- if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
- != fetchSample(i, sample, conditionVariable)) {
+ if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ
+ != fetchSample(i, mSampleHolder, conditionVariable)) {
if (mMetEos) {
// If mMetEos was on during fetchSample() due to an error,
// fetching from other tracks is not necessary.
@@ -159,6 +298,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
didSomething = true;
}
}
+ mMediaPeriod.continueLoading(mCurrentPosition);
if (!mMetEos) {
if (didSomething) {
mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
@@ -171,17 +311,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 +323,110 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
return false;
}
- private boolean prepare() {
- if (mSampleSourceReader == null) {
- mSampleSourceReader = mSampleSource.register();
- }
- if(!mSampleSourceReader.prepare(0)) {
- return false;
- }
- if (mTrackFormats == null) {
- int trackCount = mSampleSourceReader.getTrackCount();
- mTrackMetEos = new boolean[trackCount];
- List<MediaFormat> trackFormats = new ArrayList<>();
- for (int i = 0; i < trackCount; i++) {
- trackFormats.add(mSampleSourceReader.getFormat(i));
- mSampleSourceReader.enable(i, 0);
-
- }
- mTrackFormats = trackFormats;
- List<String> ids = new ArrayList<>();
- for (int i = 0; i < mTrackFormats.size(); i++) {
- ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
- }
- try {
- mSampleBuffer.init(ids, mTrackFormats);
- } catch (IOException e) {
- // In this case, we will not schedule any further operation.
- // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
- // call release() eventually.
- mExceptionOnPrepare = e;
- return false;
- }
- }
- return true;
- }
-
private int fetchSample(int track, SampleHolder sample,
ConditionVariable conditionVariable) {
- mSampleSourceReader.continueBuffering(track, mCurrentPosition);
-
- MediaFormatHolder formatHolder = new MediaFormatHolder();
- sample.clearData();
- int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample);
- if (ret == SampleSource.SAMPLE_READ) {
- if (mCurrentPosition < sample.timeUs) {
- mCurrentPosition = sample.timeUs;
+ FormatHolder dummyFormatHolder = new FormatHolder();
+ mDecoderInputBuffer.clear();
+ int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer);
+ if (ret == C.RESULT_BUFFER_READ
+ // Double-check if the extractor provided the data to prevent NPE. b/33758354
+ && mDecoderInputBuffer.data != null) {
+ if (mCurrentPosition < mDecoderInputBuffer.timeUs) {
+ mCurrentPosition = mDecoderInputBuffer.timeUs;
}
try {
Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track);
if (lastExtractedPositionUs == null) {
- mLastExtractedPositionUsMap.put(track, sample.timeUs);
+ mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs);
} else {
mLastExtractedPositionUsMap.put(track,
- Math.max(lastExtractedPositionUs, sample.timeUs));
+ Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs));
}
- queueSample(track, sample, conditionVariable);
+ queueSample(track, conditionVariable);
} catch (IOException e) {
mLastExtractedPositionUsMap.clear();
mMetEos = true;
mSampleBuffer.setEos();
}
- } else if (ret == SampleSource.END_OF_STREAM) {
+ } else if (ret == C.RESULT_END_OF_INPUT) {
mTrackMetEos[track] = true;
for (int i = 0; i < mTrackMetEos.length; ++i) {
if (!mTrackMetEos[i]) {
break;
}
- if (i == mTrackMetEos.length -1) {
+ if (i == mTrackMetEos.length - 1) {
mMetEos = true;
mSampleBuffer.setEos();
}
}
}
- // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
+ // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263
return ret;
}
- }
-
- private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
- throws IOException {
- long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
- mSampleBuffer.writeSample(index, sample, conditionVariable);
- // Checks whether the storage has enough bandwidth for recording samples.
- if (mSampleBuffer.isWriteSpeedSlow(sample.size,
- SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
- mSampleBuffer.handleWriteSpeedSlow();
+ private void queueSample(int index, ConditionVariable conditionVariable)
+ throws IOException {
+ if (mVideoTrackIndex != INVALID_TRACK_INDEX) {
+ if (!mVideoTrackMet) {
+ if (index != mVideoTrackIndex) {
+ SampleHolder sample =
+ new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY
+ : 0);
+ sample.timeUs = mDecoderInputBuffer.timeUs;
+ sample.size = mDecoderInputBuffer.data.position();
+ sample.ensureSpaceForWrite(sample.size);
+ mDecoderInputBuffer.flip();
+ sample.data.position(0);
+ sample.data.put(mDecoderInputBuffer.data);
+ sample.data.flip();
+ mPendingSamples.add(new Pair<>(index, sample));
+ return;
+ }
+ mVideoTrackMet = true;
+ mBaseSamplePts =
+ mDecoderInputBuffer.timeUs
+ - MpegTsDefaultAudioTrackRenderer
+ .INITIAL_AUDIO_BUFFERING_TIME_US;
+ for (Pair<Integer, SampleHolder> pair : mPendingSamples) {
+ if (pair.second.timeUs >= mBaseSamplePts) {
+ mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable);
+ }
+ }
+ mPendingSamples.clear();
+ } else {
+ if (mDecoderInputBuffer.timeUs < mBaseSamplePts
+ && mVideoTrackIndex != index) {
+ return;
+ }
+ }
+ }
+ // Copy the decoder input to the sample holder.
+ mSampleHolder.clearData();
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0);
+ mSampleHolder.timeUs = mDecoderInputBuffer.timeUs;
+ mSampleHolder.size = mDecoderInputBuffer.data.position();
+ mSampleHolder.ensureSpaceForWrite(mSampleHolder.size);
+ mDecoderInputBuffer.flip();
+ mSampleHolder.data.position(0);
+ mSampleHolder.data.put(mDecoderInputBuffer.data);
+ mSampleHolder.data.flip();
+ long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
+ mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable);
+
+ // Checks whether the storage has enough bandwidth for recording samples.
+ if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size,
+ SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
+ mSampleBuffer.handleWriteSpeedSlow();
+ }
}
}
@@ -328,7 +480,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
- public boolean continueBuffering(long positionUs) {
+ public boolean continueBuffering(long positionUs) {
return mSampleBuffer.continueBuffering(positionUs);
}
@@ -386,12 +538,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
private long getLastExtractedPositionUs() {
- long lastExtractedPositionUs = Long.MAX_VALUE;
- for (long value : mLastExtractedPositionUsMap.values()) {
- lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
+ long lastExtractedPositionUs = Long.MIN_VALUE;
+ for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) {
+ if (mVideoTrackIndex != entry.getKey()) {
+ lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue());
+ }
}
- if (lastExtractedPositionUs == Long.MAX_VALUE) {
- lastExtractedPositionUs = C.UNKNOWN_TIME_US;
+ if (lastExtractedPositionUs == Long.MIN_VALUE) {
+ lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US;
}
return lastExtractedPositionUs;
}
diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index ec7b4b16..b7e42a7c 100644
--- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -25,7 +25,6 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import android.os.Handler;
-import android.util.Pair;
import java.io.IOException;
import java.util.ArrayList;
@@ -61,18 +60,17 @@ public class FileSampleExtractor implements SampleExtractor{
@Override
public boolean prepare() throws IOException {
- ArrayList<Pair<String, android.media.MediaFormat>> trackInfos =
- mBufferManager.readTrackInfoFiles();
- if (trackInfos == null || trackInfos.isEmpty()) {
+ List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles();
+ if (trackFormatList == null || trackFormatList.isEmpty()) {
throw new IOException("Cannot find meta files for the recording.");
}
- mTrackCount = trackInfos.size();
+ mTrackCount = trackFormatList.size();
List<String> ids = new ArrayList<>();
mTrackFormats.clear();
for (int i = 0; i < mTrackCount; ++i) {
- Pair<String, android.media.MediaFormat> pair = trackInfos.get(i);
- ids.add(pair.first);
- mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second));
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(i);
+ ids.add(trackFormat.trackId);
+ mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format));
}
mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true,
RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index 381b22e9..2694298a 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -39,20 +39,22 @@ 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.audio.MpegTsDefaultAudioTrackRenderer;
+import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.tvinput.TunerDebug;
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,
+ MpegTsDefaultAudioTrackRenderer.EventListener,
+ MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener {
private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER;
/**
@@ -60,7 +62,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
*/
public interface RendererBuilder {
void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource,
- RendererBuilderCallback callback);
+ boolean hasSoftwareAudioDecoder, RendererBuilderCallback callback);
}
/**
@@ -94,6 +96,11 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
void onEmitCaptionEvent(CaptionEvent event);
/**
+ * Notifies clearing up whole closed caption event.
+ */
+ void onClearCaptionEvent();
+
+ /**
* Notifies the discovered caption service number.
*/
void onDiscoverCaptionServiceNumber(int serviceNumber);
@@ -215,10 +222,11 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* Creates renderers and {@link DataSource} and initializes player.
* @param context a {@link Context} instance
* @param channel to play
+ * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder
* @param eventListener for program information which will be scanned from MPEG2-TS stream
* @return true when everything is created and initialized well, false otherwise
*/
- public boolean prepare(Context context, TunerChannel channel,
+ public boolean prepare(Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder,
EventDetector.EventListener eventListener) {
TsDataSource source = null;
if (channel != null) {
@@ -236,7 +244,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING;
mBuilderCallback = new InternalRendererBuilderCallback();
- mRendererBuilder.buildRenderers(this, source, mBuilderCallback);
+ mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback);
return true;
}
@@ -304,8 +312,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 MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED,
playbackParams.getSpeed());
} else {
mPlayer.sendMessage(mAudioRenderer,
@@ -317,9 +327,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,
+ if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED,
1.0f);
} else {
mPlayer.sendMessage(mAudioRenderer,
@@ -423,8 +433,9 @@ 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 MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME,
+ volume);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
volume);
@@ -432,18 +443,20 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
- * Enables or disables audio.
+ * Enables or disables audio and closed caption.
*
- * @param enable enables the audio when {@code true}, disables otherwise.
+ * @param enable enables the audio and closed caption when {@code true}, disables otherwise.
*/
- public void setAudioTrack(boolean enable) {
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK,
+ public void setAudioTrackAndClosedCaption(boolean enable) {
+ if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) {
+ mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK,
enable ? 1 : 0);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
enable ? mVolume : 0.0f);
}
+ mPlayer.sendMessage(mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION,
+ enable);
}
/**
@@ -495,6 +508,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
+ * Returns the index of the currently selected track for the specified renderer.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @return The selected track. A negative value or a value greater than or equal to the renderer's
+ * track count indicates that the renderer is disabled.
+ */
+ public int getSelectedTrack(int rendererIndex) {
+ return mPlayer.getSelectedTrack(rendererIndex);
+ }
+
+ /**
+ * Returns the format of a track.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @param trackIndex The index of the track.
+ * @return The format of the track.
+ */
+ public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) {
+ return mPlayer.getTrackFormat(rendererIndex, trackIndex);
+ }
+
+ /**
* Gets the main handler of the player.
*/
/* package */ Handler getMainHandler() {
@@ -579,6 +614,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
@Override
public void onDroppedFrames(int count, long elapsed) {
+ TunerDebug.notifyVideoFrameDrop(count, elapsed);
if (mTrickplayRunning && mListener != null) {
mListener.onSmoothTrickplayForceStopped();
}
@@ -622,6 +658,13 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
@Override
+ public void clearCaption() {
+ if (mVideoEventListener != null) {
+ mVideoEventListener.onClearCaptionEvent();
+ }
+ }
+
+ @Override
public void discoverServiceNumber(int serviceNumber) {
if (mVideoEventListener != null) {
mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber);
@@ -650,4 +693,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..006ccac2 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -18,12 +18,14 @@ package com.android.tv.tuner.exoplayer;
import android.content.Context;
+import com.google.android.exoplayer.MediaCodecSelector;
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.audio.MpegTsDefaultAudioTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
@@ -44,7 +46,7 @@ public class MpegTsRendererBuilder implements RendererBuilder {
@Override
public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource,
- RendererBuilderCallback callback) {
+ boolean mHasSoftwareAudioDecoder, RendererBuilderCallback callback) {
// Build the video and audio renderers.
SampleExtractor extractor = dataSource == null ?
new MpegTsSampleExtractor(mBufferManager, mBufferListener) :
@@ -52,10 +54,16 @@ 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 MpegTsDefaultAudioTrackRenderer for A/V sync issue. We will use
+ // {@link MpegTsMediaCodecAudioTrackRenderer} when we use ExoPlayer's extractor.
+ TrackRenderer audioRenderer =
+ new MpegTsDefaultAudioTrackRenderer(
+ sampleSource,
+ MediaCodecSelector.DEFAULT,
+ mpegTsPlayer.getMainHandler(),
+ mpegTsPlayer,
+ mHasSoftwareAudioDecoder,
+ !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/AudioClock.java b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java
index 600c2c88..5666c5b9 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioClock.java
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.exoplayer.ac3;
+package com.android.tv.tuner.exoplayer.audio;
import com.android.tv.common.SoftPreconditions;
diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java
new file mode 100644
index 00000000..e581092a
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.exoplayer.audio;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+
+import java.nio.ByteBuffer;
+
+/** A base class for audio decoders. */
+public abstract class AudioDecoder {
+
+ /**
+ * Decodes an audio sample.
+ *
+ * @param sampleHolder a holder that contains the sample data and corresponding metadata
+ */
+ public abstract void decode(SampleHolder sampleHolder);
+
+ /** Returns a decoded sample from decoder. */
+ public abstract ByteBuffer getDecodedSample();
+
+ /** Returns the presentation time for the decoded sample. */
+ public abstract long getDecodedTimeUs();
+
+ /**
+ * Clear previous decode state if any. Prepares to decode samples of the specified encoding.
+ * This method should be called before using decode.
+ *
+ * @param mime audio encoding
+ */
+ public abstract void resetDecoderState(String mimeType);
+
+ /** Releases all the resource. */
+ public abstract void release();
+
+ /**
+ * Init decoder if needed.
+ *
+ * @param format the format used to initialize decoder
+ */
+ public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException {
+ // Do nothing.
+ }
+
+ /** Returns input buffer that will be used in decoder. */
+ public ByteBuffer getInputBuffer() {
+ return null;
+ }
+
+ /** Returns the output format. */
+ public android.media.MediaFormat getOutputFormat() {
+ return null;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java
index bfdf08ac..ec616b13 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-package com.android.tv.tuner.exoplayer.ac3;
+package com.android.tv.tuner.exoplayer.audio;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
+import com.google.android.exoplayer.util.MimeTypes;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@@ -35,7 +36,7 @@ public class AudioTrackMonitor {
private final ArrayList<Pair<Long, Integer>> mPtsList = new ArrayList<>();
private final Set<Integer> mSampleSize = new HashSet<>();
private final Set<Integer> mCurSampleSize = new HashSet<>();
- private final Set<Integer> mAc3Header = new HashSet<>();
+ private final Set<Integer> mHeader = new HashSet<>();
private long mExpireMs;
private long mDuration;
@@ -43,6 +44,8 @@ public class AudioTrackMonitor {
private long mTotalCount;
private long mStartMs;
+ private boolean mIsMp2;
+
private void flush() {
mExpireMs += mDuration;
mSampleCount = 0;
@@ -61,10 +64,14 @@ public class AudioTrackMonitor {
mTotalCount = 0;
mStartMs = 0;
mSampleSize.clear();
- mAc3Header.clear();
+ mHeader.clear();
flush();
}
+ public void setEncoding(String mime) {
+ mIsMp2 = MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mime);
+ }
+
/**
* Adds an audio sample information for monitoring.
*
@@ -76,7 +83,7 @@ public class AudioTrackMonitor {
mTotalCount++;
mSampleCount++;
mSampleSize.add(sampleSize);
- mAc3Header.add(header);
+ mHeader.add(header);
mCurSampleSize.add(sampleSize);
if (mTotalCount == 1) {
mStartMs = SystemClock.elapsedRealtime();
@@ -98,8 +105,9 @@ 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 unitDuration = mIsMp2 ? MpegTsDefaultAudioTrackRenderer.MP2_SAMPLE_DURATION_US
+ : MpegTsDefaultAudioTrackRenderer.AC3_SAMPLE_DURATION_US;
+ long sampleDuration = (mTotalCount - 1) * unitDuration / 1000;
long totalDuration = now - mStartMs;
StringBuilder ptsBuilder = new StringBuilder();
ptsBuilder.append("PTS received ").append(mSampleCount).append(", ")
@@ -113,7 +121,7 @@ public class AudioTrackMonitor {
}
if (DEBUG || mCurSampleSize.size() > 1) {
Log.d(TAG, "PTS received sample size: "
- + String.valueOf(mSampleSize) + mCurSampleSize + mAc3Header);
+ + String.valueOf(mSampleSize) + mCurSampleSize + mHeader);
}
flush();
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java
index bc3c5d00..953c9fc4 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
+++ b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.tv.tuner.exoplayer.ac3;
+package com.android.tv.tuner.exoplayer.audio;
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 =
+ MpegTsDefaultAudioTrackRenderer.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/audio/MediaCodecAudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java
new file mode 100644
index 00000000..72bc68b6
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java
@@ -0,0 +1,235 @@
+/*
+ * 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.exoplayer.audio;
+
+import android.media.MediaCodec;
+import android.util.Log;
+
+import com.google.android.exoplayer.CodecCounters;
+import com.google.android.exoplayer.DecoderInfo;
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.MediaCodecSelector;
+import com.google.android.exoplayer.MediaCodecUtil;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/** A decoder to use MediaCodec for decoding audio stream. */
+public class MediaCodecAudioDecoder extends AudioDecoder {
+ private static final String TAG = "MediaCodecAudioDecoder";
+
+ public static final int INDEX_INVALID = -1;
+
+ private final CodecCounters mCodecCounters;
+ private final MediaCodecSelector mSelector;
+
+ private MediaCodec mCodec;
+ private MediaCodec.BufferInfo mOutputBufferInfo;
+ private ByteBuffer mMediaCodecOutputBuffer;
+ private ArrayList<Long> mDecodeOnlyPresentationTimestamps;
+ private boolean mWaitingForFirstSyncFrame;
+ private boolean mIsNewIndex;
+ private int mInputIndex;
+ private int mOutputIndex;
+
+ /** Creates a MediaCodec based audio decoder. */
+ public MediaCodecAudioDecoder(MediaCodecSelector selector) {
+ mSelector = selector;
+ mOutputBufferInfo = new MediaCodec.BufferInfo();
+ mCodecCounters = new CodecCounters();
+ mDecodeOnlyPresentationTimestamps = new ArrayList<>();
+ }
+
+ /** Returns {@code true} if there is decoder for {@code mimeType}. */
+ public static boolean supportMimeType(MediaCodecSelector selector, String mimeType) {
+ if (selector == null) {
+ return false;
+ }
+ return getDecoderInfo(selector, mimeType) != null;
+ }
+
+ private static DecoderInfo getDecoderInfo(MediaCodecSelector selector, String mimeType) {
+ try {
+ return selector.getDecoderInfo(mimeType, false);
+ } catch (MediaCodecUtil.DecoderQueryException e) {
+ Log.e(TAG, "Select decoder error:" + e);
+ return null;
+ }
+ }
+
+ private boolean shouldInitCodec(MediaFormat format) {
+ return format != null && mCodec == null;
+ }
+
+ @Override
+ public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException {
+ if (!shouldInitCodec(format)) {
+ return;
+ }
+
+ String mimeType = format.mimeType;
+ DecoderInfo decoderInfo = getDecoderInfo(mSelector, mimeType);
+ if (decoderInfo == null) {
+ Log.i(TAG, "There is not decoder found for " + mimeType);
+ return;
+ }
+
+ String codecName = decoderInfo.name;
+ try {
+ mCodec = MediaCodec.createByCodecName(codecName);
+ mCodec.configure(format.getFrameworkMediaFormatV16(), null, null, 0);
+ mCodec.start();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed when configure or start codec:" + e);
+ throw new ExoPlaybackException(e);
+ }
+ mInputIndex = INDEX_INVALID;
+ mOutputIndex = INDEX_INVALID;
+ mWaitingForFirstSyncFrame = true;
+ mCodecCounters.codecInitCount++;
+ }
+
+ @Override
+ public void resetDecoderState(String mimeType) {
+ if (mCodec == null) {
+ return;
+ }
+ mInputIndex = INDEX_INVALID;
+ mOutputIndex = INDEX_INVALID;
+ mDecodeOnlyPresentationTimestamps.clear();
+ mCodec.flush();
+ mWaitingForFirstSyncFrame = true;
+ }
+
+ @Override
+ public void release() {
+ if (mCodec != null) {
+ mDecodeOnlyPresentationTimestamps.clear();
+ mInputIndex = INDEX_INVALID;
+ mOutputIndex = INDEX_INVALID;
+ mCodecCounters.codecReleaseCount++;
+ try {
+ mCodec.stop();
+ } finally {
+ try {
+ mCodec.release();
+ } finally {
+ mCodec = null;
+ }
+ }
+ }
+ }
+
+ /** Returns the index of input buffer which is ready for using. */
+ public int getInputIndex() {
+ return mInputIndex;
+ }
+
+ @Override
+ public ByteBuffer getInputBuffer() {
+ if (mInputIndex < 0) {
+ mInputIndex = mCodec.dequeueInputBuffer(0);
+ if (mInputIndex < 0) {
+ return null;
+ }
+ return mCodec.getInputBuffer(mInputIndex);
+ }
+ return mCodec.getInputBuffer(mInputIndex);
+ }
+
+ @Override
+ public void decode(SampleHolder sampleHolder) {
+ if (mWaitingForFirstSyncFrame) {
+ if (!sampleHolder.isSyncFrame()) {
+ sampleHolder.clearData();
+ return;
+ }
+ mWaitingForFirstSyncFrame = false;
+ }
+ long presentationTimeUs = sampleHolder.timeUs;
+ if (sampleHolder.isDecodeOnly()) {
+ mDecodeOnlyPresentationTimestamps.add(presentationTimeUs);
+ }
+ mCodec.queueInputBuffer(mInputIndex, 0, sampleHolder.data.limit(), presentationTimeUs, 0);
+ mInputIndex = INDEX_INVALID;
+ mCodecCounters.inputBufferCount++;
+ }
+
+ private int getDecodeOnlyIndex(long presentationTimeUs) {
+ final int size = mDecodeOnlyPresentationTimestamps.size();
+ for (int i = 0; i < size; i++) {
+ if (mDecodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) {
+ return i;
+ }
+ }
+ return INDEX_INVALID;
+ }
+
+ /** Returns the index of output buffer which is ready for using. */
+ public int getOutputIndex() {
+ if (mOutputIndex < 0) {
+ mOutputIndex = mCodec.dequeueOutputBuffer(mOutputBufferInfo, 0);
+ mIsNewIndex = true;
+ } else {
+ mIsNewIndex = false;
+ }
+ return mOutputIndex;
+ }
+
+ @Override
+ public android.media.MediaFormat getOutputFormat() {
+ return mCodec.getOutputFormat();
+ }
+
+ /** Returns {@code true} if the output is only for decoding but not for rendering. */
+ public boolean maybeDecodeOnlyIndex() {
+ int decodeOnlyIndex = getDecodeOnlyIndex(mOutputBufferInfo.presentationTimeUs);
+ if (decodeOnlyIndex != INDEX_INVALID) {
+ mCodec.releaseOutputBuffer(mOutputIndex, false);
+ mCodecCounters.skippedOutputBufferCount++;
+ mDecodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
+ mOutputIndex = INDEX_INVALID;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public ByteBuffer getDecodedSample() {
+ if (maybeDecodeOnlyIndex() || mOutputIndex < 0) {
+ return null;
+ }
+ if (mIsNewIndex) {
+ mMediaCodecOutputBuffer = mCodec.getOutputBuffer(mOutputIndex);
+ }
+ return mMediaCodecOutputBuffer;
+ }
+
+ @Override
+ public long getDecodedTimeUs() {
+ return mOutputBufferInfo.presentationTimeUs;
+ }
+
+ /** Releases the output buffer after rendering. */
+ public void releaseOutputBuffer() {
+ mCodecCounters.renderedOutputBufferCount++;
+ mCodec.releaseOutputBuffer(mOutputIndex, false);
+ mOutputIndex = INDEX_INVALID;
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
index 9dae2e34..77170419 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java
@@ -14,8 +14,10 @@
* limitations under the License.
*/
-package com.android.tv.tuner.exoplayer.ac3;
+package com.android.tv.tuner.exoplayer.audio;
+import android.media.MediaCodec;
+import android.os.Build;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Log;
@@ -23,16 +25,16 @@ 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.MediaCodecSelector;
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.exoplayer.ffmpeg.FfmpegDecoderClient;
import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
@@ -40,9 +42,10 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
- * Decodes and renders AC3 audio.
+ * Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and
+ * ffmpeg based software decoding (AC3, MP2).
*/
-public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock {
+public class MpegTsDefaultAudioTrackRenderer extends TrackRenderer implements MediaClock {
public static final int MSG_SET_VOLUME = 10000;
public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1;
public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2;
@@ -51,7 +54,19 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
// One AC3 sample has 1536 frames, and its duration is 32ms.
public static final long AC3_SAMPLE_DURATION_US = 32000;
- private static final String TAG = "Ac3PassthroughTrackRenderer";
+ // TODO: Check whether DVB broadcasting uses sample rate other than 48Khz.
+ // MPEG-1 audio Layer II and III has 1152 frames per sample.
+ // 1152 frames duration is 24ms when sample rate is 48Khz.
+ static final long MP2_SAMPLE_DURATION_US = 24000;
+
+ // 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 = "MpegTsDefaultAudioTrac";
private static final boolean DEBUG = false;
/**
@@ -67,6 +82,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
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;
+ private static final int MP2_HEADER_BITRATE_OFFSET = 2;
+ private static final int MP2_HEADER_BITRATE_MASK = 0xfc;
// Keep this as static in order to prevent new framework AudioTrack creation
// while old AudioTrack is being released.
@@ -83,17 +100,25 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
// PTS interpolated time should be delayed reasonably when AudioTrack is not used.
private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000;
+ private final MediaCodecSelector mSelector;
+
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 final boolean mAc3Passthrough;
+ private final boolean mSoftwareDecoderAvailable;
private MediaFormat mFormat;
+ private SampleHolder mSampleHolder;
+ private String mDecodingMime;
+ private boolean mFormatConfigured;
+ private int mSampleSize;
private final ByteBuffer mOutputBuffer;
+ private AudioDecoder mAudioDecoder;
private boolean mOutputReady;
private int mTrackIndex;
private boolean mSourceStateReady;
@@ -106,16 +131,23 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private long mInterpolatedTimeUs;
private long mPreviousPositionUs;
private boolean mIsStopped;
+ private boolean mEnabled = true;
+ private boolean mIsMuted;
private ArrayList<Integer> mTracksIndex;
-
- public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler,
- EventListener listener) {
+ private boolean mUseFrameworkDecoder;
+
+ public MpegTsDefaultAudioTrackRenderer(
+ SampleSource source,
+ MediaCodecSelector selector,
+ Handler eventHandler,
+ EventListener listener,
+ boolean hasSoftwareAudioDecoder,
+ boolean usePassthrough) {
mSource = source.register();
+ mSelector = selector;
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();
@@ -123,6 +155,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
mMonitor = new AudioTrackMonitor();
mAudioClock = new AudioClock();
mTracksIndex = new ArrayList<>();
+ mAc3Passthrough = usePassthrough;
+ mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable();
}
@Override
@@ -130,8 +164,11 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return this;
}
- private static boolean handlesMimeType(String mimeType) {
- return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3);
+ private boolean handlesMimeType(String mimeType) {
+ return mimeType.equals(MimeTypes.AUDIO_AC3)
+ || mimeType.equals(MimeTypes.AUDIO_E_AC3)
+ || mimeType.equals(MimeTypes.AUDIO_MPEG_L2)
+ || MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
}
@Override
@@ -141,7 +178,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
for (int i = 0; i < mSource.getTrackCount(); i++) {
- if (handlesMimeType(mSource.getFormat(i).mimeType)) {
+ String mimeType = mSource.getFormat(i).mimeType;
+ if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) {
if (mTrackIndex < 0) {
mTrackIndex = i;
}
@@ -174,7 +212,9 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
@Override
protected void onDisabled() {
- AUDIO_TRACK.resetSessionId();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ AUDIO_TRACK.resetSessionId();
+ }
clearDecodeState();
mFormat = null;
mSource.disable(mTrackIndex);
@@ -182,6 +222,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
@Override
protected void onReleased() {
+ releaseDecoder();
AUDIO_TRACK.release();
mSource.release();
}
@@ -213,9 +254,12 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
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();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ // resetSessionId() will create a new framework AudioTrack instead of reusing old one.
+ AUDIO_TRACK.resetSessionId();
+ }
seekToInternal(positionUs);
+ clearDecodeState();
}
@Override
@@ -274,7 +318,10 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return;
}
- // Process only one sample at a time for doSomeWork()
+ if (mAudioDecoder != null) {
+ mAudioDecoder.maybeInitDecoder(mFormat);
+ }
+ // Process only one sample at a time for doSomeWork() when using FFmpeg decoder.
if (processOutput()) {
if (!mOutputReady) {
while (feedInputBuffer()) {
@@ -314,9 +361,18 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private void clearDecodeState() {
mOutputReady = false;
+ if (mAudioDecoder != null) {
+ mAudioDecoder.resetDecoderState(mDecodingMime);
+ }
AUDIO_TRACK.reset();
}
+ private void releaseDecoder() {
+ if (mAudioDecoder != null) {
+ mAudioDecoder.release();
+ }
+ }
+
private void readFormat() throws IOException, ExoPlaybackException {
int result = mSource.readData(mTrackIndex, mCurrentPositionUs,
mFormatHolder, mSampleHolder);
@@ -325,14 +381,69 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
}
}
+ private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
+ return MediaFormat.createAudioFormat(
+ format.trackId,
+ MimeTypes.AUDIO_RAW,
+ format.bitrate,
+ format.maxInputSize,
+ format.durationUs,
+ format.channelCount,
+ format.sampleRate,
+ format.initializationData,
+ format.language);
+ }
+
private void onInputFormatChanged(MediaFormatHolder formatHolder)
throws ExoPlaybackException {
- mFormat = formatHolder.format;
- if (DEBUG) {
+ String mimeType = formatHolder.format.mimeType;
+ mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType);
+ if (mUseFrameworkDecoder) {
+ mAudioDecoder = new MediaCodecAudioDecoder(mSelector);
+ mFormat = formatHolder.format;
+ mAudioDecoder.maybeInitDecoder(mFormat);
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
+ } else if (mSoftwareDecoderAvailable
+ && (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mimeType)
+ || MimeTypes.AUDIO_AC3.equalsIgnoreCase(mimeType) && !mAc3Passthrough)) {
+ releaseDecoder();
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
+ mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
+ mAudioDecoder = FfmpegDecoderClient.getInstance();
+ mDecodingMime = mimeType;
+ mFormat = convertMediaFormatToRaw(formatHolder.format);
+ } else {
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT);
+ mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE);
+ mFormat = formatHolder.format;
+ releaseDecoder();
+ }
+ mFormatConfigured = true;
+ mMonitor.setEncoding(mimeType);
+ if (DEBUG && !mUseFrameworkDecoder) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString());
}
clearDecodeState();
- AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16());
+ if (!mUseFrameworkDecoder) {
+ 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 void onOutputFormatChanged(android.media.MediaFormat format) {
+ if (DEBUG) {
+ Log.d(TAG, "AudioTrack was configured to FORMAT: " + format.toString());
+ }
+ AUDIO_TRACK.reconfigure(format, 0);
}
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
@@ -340,10 +451,24 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
- mSampleHolder.data.clear();
- mSampleHolder.size = 0;
- int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder,
- mSampleHolder);
+ if (mUseFrameworkDecoder) {
+ boolean indexChanged =
+ ((MediaCodecAudioDecoder) mAudioDecoder).getInputIndex()
+ == MediaCodecAudioDecoder.INDEX_INVALID;
+ if (indexChanged) {
+ mSampleHolder.data = mAudioDecoder.getInputBuffer();
+ if (mSampleHolder.data != null) {
+ mSampleHolder.clearData();
+ } else {
+ return false;
+ }
+ }
+ } else {
+ mSampleHolder.data.clear();
+ mSampleHolder.size = 0;
+ }
+ int result =
+ mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder);
switch (result) {
case SampleSource.NOTHING_READ: {
return false;
@@ -359,8 +484,48 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
default: {
+ if (mSampleHolder.size != mSampleSize
+ && mFormatConfigured
+ && !mUseFrameworkDecoder) {
+ onSampleSizeChanged(mSampleHolder.size);
+ }
mSampleHolder.data.flip();
- decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
+ if (!mUseFrameworkDecoder) {
+ if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) {
+ mMonitor.addPts(
+ mSampleHolder.timeUs,
+ mOutputBuffer.position(),
+ mSampleHolder.data.get(MP2_HEADER_BITRATE_OFFSET)
+ & MP2_HEADER_BITRATE_MASK);
+ } else {
+ mMonitor.addPts(
+ mSampleHolder.timeUs,
+ mOutputBuffer.position(),
+ mSampleHolder.data.get(AC3_HEADER_BITRATE_OFFSET) & 0xff);
+ }
+ }
+ if (mAudioDecoder != null) {
+ mAudioDecoder.decode(mSampleHolder);
+ if (mUseFrameworkDecoder) {
+ int outputIndex =
+ ((MediaCodecAudioDecoder) mAudioDecoder).getOutputIndex();
+ if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ onOutputFormatChanged(mAudioDecoder.getOutputFormat());
+ return true;
+ } else if (outputIndex < 0) {
+ return true;
+ }
+ if (((MediaCodecAudioDecoder) mAudioDecoder).maybeDecodeOnlyIndex()) {
+ AUDIO_TRACK.handleDiscontinuity();
+ return true;
+ }
+ }
+ ByteBuffer outputBuffer = mAudioDecoder.getDecodedSample();
+ long presentationTimeUs = mAudioDecoder.getDecodedTimeUs();
+ decodeDone(outputBuffer, presentationTimeUs);
+ } else {
+ decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
+ }
return true;
}
}
@@ -383,15 +548,22 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
int handleBufferResult;
try {
// To reduce discontinuity, interpolate presentation time.
- mInterpolatedTimeUs = mPresentationTimeUs
+ if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) {
+ mInterpolatedTimeUs = mPresentationTimeUs
+ + mPresentationCount * MP2_SAMPLE_DURATION_US;
+ } else if (!mUseFrameworkDecoder) {
+ mInterpolatedTimeUs = mPresentationTimeUs
+ mPresentationCount * AC3_SAMPLE_DURATION_US;
- handleBufferResult = AUDIO_TRACK.handleBuffer(mOutputBuffer,
- 0, mOutputBuffer.limit(), mInterpolatedTimeUs);
+ } else {
+ mInterpolatedTimeUs = mPresentationTimeUs;
+ }
+ 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;
@@ -399,6 +571,9 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
mCodecCounters.renderedOutputBufferCount++;
mOutputReady = false;
+ if (mUseFrameworkDecoder) {
+ ((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer();
+ }
return true;
}
return false;
@@ -421,7 +596,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
if (!AUDIO_TRACK.isInitialized()) {
return mAudioClock.getPositionUs();
} else if (!AUDIO_TRACK.isEnabled()) {
- if (mInterpolatedTimeUs > 0) {
+ if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) {
return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US;
}
return mPresentationTimeUs;
@@ -471,8 +646,6 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
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 {
@@ -511,24 +684,29 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_SET_VOLUME:
- AUDIO_TRACK.setVolume((Float) message);
+ float volume = (Float) message;
+ // Workaround: we cannot mute the audio track by setting the volume to 0, we need to
+ // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track
+ // whenever volume is being set might cause side effects, therefore we only handle
+ // "explicit mute operations", i.e., only after certain non-zero volume has been
+ // set, the subsequent volume setting operations will be consider as mute/un-mute
+ // operations and thus enable/disable the audio track.
+ if (mIsMuted && volume > 0) {
+ mIsMuted = false;
+ if (mEnabled) {
+ setStatus(true);
+ }
+ } else if (!mIsMuted && volume == 0) {
+ mIsMuted = true;
+ if (mEnabled) {
+ setStatus(false);
+ }
+ }
+ AUDIO_TRACK.setVolume(volume);
break;
case MSG_SET_AUDIO_TRACK:
- boolean enabled = (Integer) message == 1;
- if (enabled == AUDIO_TRACK.isEnabled()) {
- return;
- }
- if (!enabled) {
- // mAudioClock can be different from getPositionUs. In order to sync them,
- // we set mAudioClock.
- mAudioClock.setPositionUs(getPositionUs());
- }
- AUDIO_TRACK.setStatus(enabled);
- if (enabled) {
- // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
- // the current position. If not, AUDIO_TRACK has the obsolete data.
- seekTo(mAudioClock.getPositionUs());
- }
+ mEnabled = (Integer) message == 1;
+ setStatus(mEnabled);
break;
case MSG_SET_PLAYBACK_SPEED:
mAudioClock.setPlaybackSpeed((Float) message);
@@ -537,4 +715,21 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
super.handleMessage(messageType, message);
}
}
+
+ private void setStatus(boolean enabled) {
+ if (enabled == AUDIO_TRACK.isEnabled()) {
+ return;
+ }
+ if (!enabled) {
+ // mAudioClock can be different from getPositionUs. In order to sync them,
+ // we set mAudioClock.
+ mAudioClock.setPositionUs(getPositionUs());
+ }
+ AUDIO_TRACK.setStatus(enabled);
+ if (enabled) {
+ // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
+ // the current position. If not, AUDIO_TRACK has the obsolete data.
+ seekTo(mAudioClock.getPositionUs());
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
index 2bf86b5a..142aa9b2 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.exoplayer.ac3;
+package com.android.tv.tuner.exoplayer.audio;
import android.os.Handler;
@@ -25,16 +25,13 @@ import com.google.android.exoplayer.SampleSource;
/**
* MPEG-2 TS audio track renderer.
- * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at
- * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
- * asynchronous Audio/Video outputs.
- * This class calculates the offset of audio data and adjust the presentation times to avoid the
- * asynchronous Audio/Video problem.
+ *
+ * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the
+ * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
+ * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the
+ * presentation times to avoid the asynchronous Audio/Video problem.
*/
-public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
- private final String TAG = "Ac3TrackRenderer";
- private final boolean DEBUG = false;
-
+public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRenderer {
private final Ac3EventListener mListener;
public interface Ac3EventListener extends EventListener {
@@ -47,8 +44,11 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e);
}
- public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
- Handler eventHandler, EventListener eventListener) {
+ public MpegTsMediaCodecAudioTrackRenderer(
+ SampleSource source,
+ MediaCodecSelector mediaCodecSelector,
+ Handler eventHandler,
+ EventListener eventListener) {
super(source, mediaCodecSelector, eventHandler, eventListener);
mListener = (Ac3EventListener) eventListener;
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index eb596e93..112e9dc4 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -25,13 +25,14 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.util.Utils;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -59,7 +60,8 @@ public class BufferManager {
private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
// Maps from track name to a map which maps from starting position to {@link SampleChunk}.
- private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>();
+ private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap =
+ new ArrayMap<>();
private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
private final StorageManager mStorageManager;
@@ -77,13 +79,11 @@ public class BufferManager {
}
};
- private volatile boolean mClosed = false;
private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
private long mTotalWriteSize;
private long mTotalWriteTimeNs;
private float mWriteBandwidth = 0.0f;
private volatile int mSpeedCheckCount;
- private boolean mDisabled = false;
public interface ChunkEvictedListener {
void onChunkEvicted(String id, long createdTimeMs);
@@ -174,6 +174,66 @@ public class BufferManager {
}
/**
+ * A Track format which will be loaded and saved from the permanent storage for recordings.
+ */
+ public static class TrackFormat {
+
+ /**
+ * The track id for the specified track. The track id will be used as a track identifier
+ * for recordings.
+ */
+ public final String trackId;
+
+ /**
+ * The {@link MediaFormat} for the specified track.
+ */
+ public final MediaFormat format;
+
+ /**
+ * Creates TrackFormat.
+ * @param trackId
+ * @param format
+ */
+ public TrackFormat(String trackId, MediaFormat format) {
+ this.trackId = trackId;
+ this.format = format;
+ }
+ }
+
+ /**
+ * A Holder for a sample position which will be loaded from the index file for recordings.
+ */
+ public static class PositionHolder {
+
+ /**
+ * The current sample position in microseconds.
+ * The position is identical to the PTS(presentation time stamp) of the sample.
+ */
+ public final long positionUs;
+
+ /**
+ * Base sample position for the current {@link SampleChunk}.
+ */
+ public final long basePositionUs;
+
+ /**
+ * The file offset for the current sample in the current {@link SampleChunk}.
+ */
+ public final int offset;
+
+ /**
+ * Creates a holder for a specific position in the recording.
+ * @param positionUs
+ * @param offset
+ */
+ public PositionHolder(long positionUs, long basePositionUs, int offset) {
+ this.positionUs = positionUs;
+ this.basePositionUs = basePositionUs;
+ this.offset = offset;
+ }
+ }
+
+ /**
* Storage configuration and policy manager for {@link BufferManager}
*/
public interface StorageManager {
@@ -186,11 +246,6 @@ public class BufferManager {
File getBufferDir();
/**
- * Cleans up storage.
- */
- void clearStorage();
-
- /**
* Informs whether the storage is used for persistent use. (eg. dvr recording/play)
*
* @return {@code true} if stored files are persistent
@@ -220,29 +275,27 @@ public class BufferManager {
* Reads track name & {@link MediaFormat} from storage.
*
* @param isAudio {@code true} if it is for audio track
- * @return {@link Pair} of track name & {@link MediaFormat}
- * @throws IOException
+ * @return {@link List} of TrackFormat
*/
- Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
+ List<TrackFormat> readTrackInfoFiles(boolean isAudio);
/**
- * Reads sample indexes for each written sample from storage.
+ * Reads key sample positions for each written sample from storage.
*
* @param trackId track name
* @return indexes of the specified track
* @throws IOException
*/
- ArrayList<Long> readIndexFile(String trackId) throws IOException;
+ ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
/**
* Writes track information to storage.
*
- * @param trackId track name
- * @param format {@link android.media.MediaFormat} of the track
+ * @param formatList {@list List} of TrackFormat
* @param isAudio {@code true} if it is for audio track
* @throws IOException
*/
- void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)
throws IOException;
/**
@@ -252,7 +305,7 @@ public class BufferManager {
* @param index {@link SampleChunk} container
* @throws IOException
*/
- void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException;
}
@@ -307,7 +360,6 @@ public class BufferManager {
SampleChunk.SampleChunkCreator sampleChunkCreator) {
mStorageManager = storageManager;
mSampleChunkCreator = sampleChunkCreator;
- clearBuffer(true);
}
public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
@@ -318,44 +370,44 @@ public class BufferManager {
mEvictListeners.remove(id);
}
- private void clearBuffer(boolean deleteFiles) {
- mChunkMap.clear();
- if (deleteFiles) {
- mStorageManager.clearStorage();
- }
- mBufferSize = 0;
- }
-
private static String getFileName(String id, long positionUs) {
return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
}
/**
- * Creates a new {@link SampleChunk} for caching samples.
+ * Creates a new {@link SampleChunk} for caching samples if it is needed.
*
* @param id the name of the track
- * @param positionUs starting position of the {@link SampleChunk} in micro seconds.
+ * @param positionUs current position to write a sample in micro seconds.
* @param samplePool {@link SamplePool} for the fast creation of samples.
+ * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create
+ * a new {@link SampleChunk}.
+ * @param currentOffset the current offset to write.
* @return returns the created {@link SampleChunk}.
* @throws IOException
*/
- public SampleChunk createNewWriteFile(String id, long positionUs,
- SamplePool samplePool) throws IOException {
+ public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool,
+ SampleChunk currentChunk, int currentOffset) throws IOException {
if (!maybeEvictChunk()) {
throw new IOException("Not enough storage space");
}
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(id, map);
mStartPositionMap.put(id, positionUs);
mPendingDelete.init(id);
}
- File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
- SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file,
- positionUs, mChunkCallback);
- map.put(positionUs, sampleChunk);
- return sampleChunk;
+ if (currentChunk == null) {
+ File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
+ SampleChunk sampleChunk = mSampleChunkCreator
+ .createSampleChunk(samplePool, file, positionUs, mChunkCallback);
+ map.put(positionUs, new Pair(sampleChunk, 0));
+ return sampleChunk;
+ } else {
+ map.put(positionUs, new Pair(currentChunk, currentOffset));
+ return null;
+ }
}
/**
@@ -366,10 +418,10 @@ public class BufferManager {
* @throws IOException
*/
public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
- ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
- long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0;
+ ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
+ long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
- SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(trackId, map);
@@ -377,11 +429,15 @@ public class BufferManager {
mPendingDelete.init(trackId);
}
SampleChunk chunk = null;
- for (long positionUs: keyPositions) {
- chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
- mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs,
- mChunkCallback, chunk);
- map.put(positionUs, chunk);
+ long basePositionUs = -1;
+ for (PositionHolder position: keyPositions) {
+ if (position.basePositionUs != basePositionUs) {
+ chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
+ mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs),
+ position.positionUs, mChunkCallback, chunk);
+ basePositionUs = position.basePositionUs;
+ }
+ map.put(position.positionUs, new Pair(chunk, position.offset));
}
}
@@ -392,19 +448,19 @@ public class BufferManager {
* @param positionUs the position.
* @return returns the found {@link SampleChunk}.
*/
- public SampleChunk getReadFile(String id, long positionUs) {
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
return null;
}
- SampleChunk sampleChunk;
- SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1);
+ Pair<SampleChunk, Integer> ret;
+ SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
if (!headMap.isEmpty()) {
- sampleChunk = headMap.get(headMap.lastKey());
+ ret = headMap.get(headMap.lastKey());
} else {
- sampleChunk = map.get(map.firstKey());
+ ret = map.get(map.firstKey());
}
- return sampleChunk;
+ return ret;
}
/**
@@ -439,15 +495,16 @@ public class BufferManager {
// Since chunks are persistent, we cannot evict chunks.
return false;
}
- SortedMap<Long, SampleChunk> earliestChunkMap = null;
+ SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
SampleChunk earliestChunk = null;
String earliestChunkId = null;
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
- SampleChunk chunk = map.get(map.firstKey());
+ SampleChunk chunk = map.get(map.firstKey()).first;
if (earliestChunk == null
|| chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
earliestChunkMap = map;
@@ -473,8 +530,9 @@ public class BufferManager {
}
pendingDelete = mPendingDelete.getSize();
}
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
@@ -489,70 +547,74 @@ public class BufferManager {
* @return returns all track information which is found by {@link BufferManager.StorageManager}.
* @throws IOException
*/
- public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
- ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(false));
- } catch (FileNotFoundException e) {
- // There can be a single track only recording. (eg. audio-only, video-only)
- // So the exception should not stop the read.
+ public List<TrackFormat> readTrackInfoFiles() throws IOException {
+ List<TrackFormat> trackFormatList = new ArrayList<>();
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
+ if (trackFormatList.isEmpty()) {
+ throw new IOException("No track information to load");
}
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(true));
- } catch (FileNotFoundException e) {
- // See above catch block.
- }
- return trackInfos;
+ return trackFormatList;
}
/**
* Writes track information and index information for all tracks.
*
- * @param audio audio information.
- * @param video video information.
+ * @param audios list of audio track information
+ * @param videos list of audio track information
* @throws IOException
*/
- public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)
+ public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
throws IOException {
- if (audio != null) {
- mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first);
- if (map == null) {
- throw new IOException("Audio track index missing");
+ if (audios.isEmpty() && videos.isEmpty()) {
+ throw new IOException("No track information to save");
+ }
+ if (!audios.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(audios, true);
+ for (TrackFormat trackFormat : audios) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Audio track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(audio.first, map);
}
- if (video != null) {
- mStorageManager.writeTrackInfoFile(video.first, video.second, false);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first);
- if (map == null) {
- throw new IOException("Video track index missing");
+ if (!videos.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(videos, false);
+ for (TrackFormat trackFormat : videos) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Video track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(video.first, map);
}
}
/**
- * Marks it is closed and it is not used anymore.
- */
- public void close() {
- // Clean-up may happen after this is called.
- mClosed = true;
- }
-
- /**
* Releases all the resources.
*/
public void release() {
- mPendingDelete.release();
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- for (SampleChunk chunk : entry.getValue().values()) {
- SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
+ try {
+ mPendingDelete.release();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SampleChunk toRelease = null;
+ for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
+ if (toRelease != positions.first) {
+ toRelease = positions.first;
+ SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
+ }
+ }
}
- }
- mChunkMap.clear();
- if (mClosed) {
- clearBuffer(!mStorageManager.isPersistent());
+ mChunkMap.clear();
+ } catch (ConcurrentModificationException | NullPointerException e) {
+ // TODO: remove this after it it confirmed that race condition issues are resolved.
+ // b/32492258, b/32373376
+ SoftPreconditions.checkState(false, "Exception on BufferManager#release: ",
+ e.toString());
}
}
@@ -611,20 +673,6 @@ public class BufferManager {
}
/**
- * Marks {@link BufferManager} object disabled to prevent it from the future use.
- */
- public void disable() {
- mDisabled = true;
- }
-
- /**
- * Returns if {@link BufferManager} object is disabled.
- */
- public boolean isDisabled() {
- return mDisabled;
- }
-
- /**
* Returns if {@link BufferManager} has checked the write speed,
* which is suitable for Trickplay.
*/
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 6a0502a7..6a09016c 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.nano.Track.AtscCaptionTrack;
+import com.google.protobuf.nano.MessageNano;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -28,18 +32,25 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.SortedMap;
/**
* Manages DVR storage.
*/
public class DvrStorageManager implements BufferManager.StorageManager {
+ private static final String TAG = "DvrStorageManager";
// TODO: make serializable classes and use protobuf after internal data structure is finalized.
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
+ private static final String META_FILE_TYPE_AUDIO = "audio";
+ private static final String META_FILE_TYPE_VIDEO = "video";
+ private static final String META_FILE_TYPE_CAPTION = "caption";
private static final String META_FILE_SUFFIX = ".meta";
private static final String IDX_FILE_SUFFIX = ".idx";
+ private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
// Size of minimum reserved storage buffer which will be used to save meta files
// and index files after actual recording finished.
@@ -59,18 +70,6 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void clearStorage() {
- if (mIsRecording) {
- File[] files = mBufferDir.listFiles();
- if (files != null && files.length > 0) {
- for (File file : files) {
- file.delete();
- }
- }
- }
- }
-
- @Override
public File getBufferDir() {
return mBufferDir;
}
@@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
}
+ private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
+ try {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ } catch (IOException e) {
+ // Since we are reading optional field, ignore the exception.
+ }
+ }
+
private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
int len = in.readInt();
if (len <= 0) {
@@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
- String name = readString(in);
- MediaFormat format = new MediaFormat();
- readFormatString(in, format, MediaFormat.KEY_MIME);
- readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- readFormatInt(in, format, MediaFormat.KEY_WIDTH);
- readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
- readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
- readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
- readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- readFormatByteBuffer(in, format, "csd-" + i);
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ String name = readString(in);
+ MediaFormat format = new MediaFormat();
+ readFormatString(in, format, MediaFormat.KEY_MIME);
+ readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ readFormatInt(in, format, MediaFormat.KEY_WIDTH);
+ readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
+ readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
+ readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
+ readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int i = 0; i < 3; ++i) {
+ readFormatByteBuffer(in, format, "csd-" + i);
+ }
+ readFormatLong(in, format, MediaFormat.KEY_DURATION);
+
+ // This is optional since language field is added later.
+ readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
+ trackFormatList.add(new BufferManager.TrackFormat(name, format));
+ } catch (IOException e) {
+ trackNotFound = true;
}
- readFormatLong(in, format, MediaFormat.KEY_DURATION);
- return new Pair<>(name, format);
+ index++;
+ } while(!trackNotFound);
+ return trackFormatList;
+ }
+
+ /**
+ * Reads caption information from files.
+ *
+ * @return a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public List<AtscCaptionTrack> readCaptionInfoFiles() {
+ List<AtscCaptionTrack> tracks = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ byte[] data = new byte[(int) file.length()];
+ in.read(data);
+ tracks.add(AtscCaptionTrack.parseFrom(data));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while(!trackNotFound);
+ return tracks;
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
+ }
+ return indices;
}
}
- @Override
- public ArrayList<Long> readIndexFile(String trackId) throws IOException {
- ArrayList<Long> indices = new ArrayList<>();
- File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
long count = in.readLong();
for (long i = 0; i < count; ++i) {
- indices.add(in.readLong());
+ long positionUs = in.readLong();
+ long basePositionUs = in.readLong();
+ int offset = in.readInt();
+ indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
}
return indices;
}
}
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+ throws IOException {
+ File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
+ if (file.exists()) {
+ return readNewIndexFile(file);
+ } else {
+ return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX));
+ }
+ }
+
private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
throws IOException {
if (format.containsKey(key)) {
@@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
- writeString(out, trackId);
- writeFormatString(out, format, MediaFormat.KEY_MIME);
- writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- writeFormatInt(out, format, MediaFormat.KEY_WIDTH);
- writeFormatInt(out, format, MediaFormat.KEY_HEIGHT);
- writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT);
- writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE);
- writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- writeFormatByteBuffer(out, format, "csd-" + i);
+ for (int i = 0; i < formatList.size() ; ++i) {
+ BufferManager.TrackFormat trackFormat = formatList.get(i);
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ writeString(out, trackFormat.trackId);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
+ writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int j = 0; j < 3; ++j) {
+ writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
+ }
+ writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
+ }
+ }
+ }
+
+ /**
+ * Writes caption information to files.
+ *
+ * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
+ if (tracks == null || tracks.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < tracks.size(); i++) {
+ AtscCaptionTrack track = tracks.get(i);
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ out.write(MessageNano.toByteArray(track));
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to write caption info to files", e);
}
- writeFormatLong(out, format, MediaFormat.KEY_DURATION);
}
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException {
- File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX);
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
out.writeLong(index.size());
- for (Long key : index.keySet()) {
- out.writeLong(key);
+ for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
+ out.writeLong(entry.getKey());
+ out.writeLong(entry.getValue().first.getStartPositionUs());
+ out.writeInt(entry.getValue().second);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index 4869b49f..af0c3f0d 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public static final int BUFFER_REASON_RECORDING = 2;
/**
- * The duration of a chunk of samples, {@link SampleChunk}.
+ * The minimum duration to support seek in Trickplay.
*/
- static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+ static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+
+ /**
+ * The duration of a {@link SampleChunk} for recordings.
+ */
+ static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes
private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds
private static final long BUFFER_NEEDED_US =
1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
@@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private int mTrackCount;
private boolean[] mTrackSelected;
- private List<String> mIds;
private List<SampleQueue> mReadSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
@@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackCount <= 0) {
throw new IOException("No tracks to initialize");
}
- mIds = ids;
mTrackSelected = new boolean[mTrackCount];
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
@@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
mSampleChunkIoHelper.init();
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
+ }
}
@Override
@@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (!mTrackSelected[index]) {
mTrackSelected[index] = true;
mReadSampleQueues.get(index).clear();
- mBufferManager.registerChunkEvictedListener(mIds.get(index),
- RecordingSampleBuffer.this);
mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
}
}
@@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackSelected[index]) {
mTrackSelected[index] = false;
mReadSampleQueues.get(index).clear();
- mBufferManager.unregisterChunkEvictedListener(mIds.get(index));
+ mSampleChunkIoHelper.closeRead(index);
}
}
@@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
Log.w(TAG, "Disk is too slow for trickplay");
- mBufferManager.disable();
mBufferListener.onDiskTooSlow();
}
@@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private boolean maybeReadSample(SampleQueue queue, int index) {
if (queue.getLastQueuedPositionUs() != null
&& queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
- && queue.isDurationGreaterThan(CHUNK_DURATION_US)) {
+ && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) {
// The speed of queuing samples can be higher than the playback speed.
// If the duration of the samples in the queue is not limited,
// samples can be accumulated and there can be out-of-memory issues.
@@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public void onChunkEvicted(String id, long createdTimeMs) {
if (mBufferListener != null) {
mBufferListener.onBufferStartTimeChanged(
- createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
+ createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
index 552caaef..04b5a071 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -78,7 +78,6 @@ public class SampleChunk {
/**
* A class for SampleChunk creation.
*/
- @VisibleForTesting
public static class SampleChunkCreator {
/**
@@ -151,18 +150,23 @@ public class SampleChunk {
mCurrentOffset = 0;
}
+ private void reset(SampleChunk chunk, long offset) {
+ mChunk = chunk;
+ mCurrentOffset = offset;
+ }
+
/**
* Prepares for read I/O operation from a new SampleChunk.
*
* @param chunk the new SampleChunk to read from
* @throws IOException
*/
- void openRead(SampleChunk chunk) throws IOException {
+ void openRead(SampleChunk chunk, long offset) throws IOException {
if (mChunk != null) {
mChunk.closeRead();
}
chunk.openRead();
- reset(chunk);
+ reset(chunk, offset);
}
/**
@@ -241,6 +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.
*
* @param chunk to release
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index 37ae4022..ca97a91a 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -21,6 +21,7 @@ import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import java.io.IOException;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
@@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback {
private static final int MSG_OPEN_READ = 1;
private static final int MSG_OPEN_WRITE = 2;
- private static final int MSG_CLOSE_WRITE = 3;
- private static final int MSG_READ = 4;
- private static final int MSG_WRITE = 5;
- private static final int MSG_RELEASE = 6;
+ private static final int MSG_CLOSE_READ = 3;
+ private static final int MSG_CLOSE_WRITE = 4;
+ private static final int MSG_READ = 5;
+ private static final int MSG_WRITE = 6;
+ private static final int MSG_RELEASE = 7;
+ private final long mSampleChunkDurationUs;
private final int mTrackCount;
private final List<String> mIds;
private final List<MediaFormat> mMediaFormats;
@@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback {
private Handler mIoHandler;
private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[];
- private final long[] mWriteEndPositionUs;
+ private final long[] mWriteIndexEndPositionUs;
+ private final long[] mWriteChunkEndPositionUs;
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
+ private final Set<Integer> mSelectedTracks = new ArraySet<>();
private long mBufferDurationUs = 0;
private boolean mWriteEnded;
private boolean mErrorNotified;
@@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback {
mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
- mWriteEndPositionUs = new long[mTrackCount];
+ mWriteIndexEndPositionUs = new long[mTrackCount];
+ mWriteChunkEndPositionUs = new long[mTrackCount];
mReadIoStates = new SampleChunk.IoState[mTrackCount];
mWriteIoStates = new SampleChunk.IoState[mTrackCount];
+
+ // Small chunk duration for live playback will give more fine grained storage usage
+ // and eviction handling for trickplay.
+ mSampleChunkDurationUs =
+ bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ?
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US :
+ RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US;
for (int i = 0; i < mTrackCount; ++i) {
- mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US;
+ mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs;
mReadIoStates[i] = new SampleChunk.IoState();
mWriteIoStates[i] = new SampleChunk.IoState();
}
@@ -204,6 +220,15 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
/**
+ * Closes read from the specified track.
+ *
+ * @param index track index
+ */
+ public void closeRead(int index) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index));
+ }
+
+ /**
* Notifies writes are finished.
*/
public void closeWrite() {
@@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback {
try {
if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
// Saves meta information for recording.
- Pair<String, android.media.MediaFormat> audio = null, video = null;
+ List<BufferManager.TrackFormat> audios = new LinkedList<>();
+ List<BufferManager.TrackFormat> videos = new LinkedList<>();
for (int i = 0; i < mTrackCount; ++i) {
android.media.MediaFormat format =
mMediaFormats.get(i).getFrameworkMediaFormatV16();
format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
- if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
- audio = new Pair<>(mIds.get(i), format);
- } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
- video = new Pair<>(mIds.get(i), format);
- }
- if (audio != null && video != null) {
- break;
+ if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+ audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+ videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
}
}
- mBufferManager.writeMetaFiles(audio, video);
+ mBufferManager.writeMetaFiles(audios, videos);
}
} finally {
mBufferManager.release();
@@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback {
case MSG_OPEN_WRITE:
doOpenWrite((int) message.obj);
return true;
+ case MSG_CLOSE_READ:
+ doCloseRead((int) message.obj);
+ return true;
case MSG_CLOSE_WRITE:
doCloseWrite();
return true;
@@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback {
private void doOpenRead(IoParams params) throws IOException {
int index = params.index;
mIoHandler.removeMessages(MSG_READ, index);
- SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs);
- if (chunk == null) {
+ Pair<SampleChunk, Integer> readPosition =
+ mBufferManager.getReadFile(mIds.get(index), params.positionUs);
+ if (readPosition == null) {
String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs
+ "is not found";
- SoftPreconditions.checkNotNull(chunk, TAG, errorMessage);
+ SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
throw new IOException(errorMessage);
}
- mReadIoStates[index].openRead(chunk);
+ mSelectedTracks.add(index);
+ mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second);
if (mHandlerReadSampleBuffers[index] != null) {
SampleHolder sample;
while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
@@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doOpenWrite(int index) throws IOException {
- SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool);
+ SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0,
+ mSamplePool, null, 0);
mWriteIoStates[index].openWrite(chunk);
}
+ private void doCloseRead(int index) {
+ mSelectedTracks.remove(index);
+ if (mHandlerReadSampleBuffers[index] != null) {
+ SampleHolder sample;
+ while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
+ mSamplePool.releaseSample(sample);
+ }
+ }
+ mIoHandler.removeMessages(MSG_READ, index);
+ }
+
private void doRead(int index) throws IOException {
mIoHandler.removeMessages(MSG_READ, index);
if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) {
@@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback {
if (sample.timeUs > mBufferDurationUs) {
mBufferDurationUs = sample.timeUs;
}
-
- if (sample.timeUs >= mWriteEndPositionUs[index]) {
- nextChunk = mBufferManager.createNewWriteFile(mIds.get(index),
- mWriteEndPositionUs[index], mSamplePool);
- mWriteEndPositionUs[index] =
- ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) *
- RecordingSampleBuffer.CHUNK_DURATION_US;
+ if (sample.timeUs >= mWriteIndexEndPositionUs[index]) {
+ SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ?
+ null : mWriteIoStates[params.index].getChunk();
+ int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+ nextChunk = mBufferManager.createNewWriteFileIfNeeded(
+ mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool,
+ currentChunk, currentOffset);
+ mWriteIndexEndPositionUs[index] =
+ ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) *
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ if (nextChunk != null) {
+ mWriteChunkEndPositionUs[index] =
+ ((sample.timeUs / mSampleChunkDurationUs) + 1)
+ * mSampleChunkDurationUs;
+ }
}
}
mWriteIoStates[params.index].write(params.sample, nextChunk);
@@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.removeCallbacksAndMessages(null);
mFinished = true;
conditionVariable.open();
+ mSelectedTracks.clear();
}
private void releaseEvictedChunks() {
- if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) {
+ if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK
+ || mSelectedTracks.isEmpty()) {
return;
}
+ long currentStartPositionUs = Long.MAX_VALUE;
+ for (int trackIndex : mSelectedTracks) {
+ currentStartPositionUs = Math.min(currentStartPositionUs,
+ mReadIoStates[trackIndex].getStartPositionUs());
+ }
for (int i = 0; i < mTrackCount; ++i) {
long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)),
- mReadIoStates[i].getStartPositionUs());
+ currentStartPositionUs);
mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs);
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
index 7b098f40..75eac5a2 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
@@ -43,6 +43,7 @@ public class SampleQueue {
if (sampleFromQueue == null) {
return SampleSource.NOTHING_READ;
}
+ sample.ensureSpaceForWrite(sampleFromQueue.size);
sample.size = sampleFromQueue.size;
sample.flags = sampleFromQueue.flags;
sample.timeUs = sampleFromQueue.timeUs;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 40c4ef95..159fde18 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -19,18 +19,18 @@ 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;
import com.google.android.exoplayer.SampleSource;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import java.io.IOException;
import java.util.List;
-import junit.framework.Assert;
-
/**
* Handles I/O for {@link SampleExtractor} when
* physical storage based buffer is not used. Trickplay is disabled.
@@ -115,8 +115,8 @@ public class SimpleSampleBuffer implements BufferManager.SampleBuffer {
@Override
public synchronized int readSample(int track, SampleHolder sampleHolder) {
SampleQueue queue = mPlayingSampleQueues[track];
- Assert.assertNotNull(queue);
- int result = queue.dequeueSample(sampleHolder);
+ SoftPreconditions.checkNotNull(queue);
+ int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder);
if (result != SampleSource.SAMPLE_READ && reachedEos()) {
return SampleSource.END_OF_STREAM;
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index 258a5cd0..9fe921b8 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -17,20 +17,23 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.content.Context;
-import android.media.MediaFormat;
import android.os.AsyncTask;
-import android.os.Looper;
import android.provider.Settings;
+import android.support.annotation.NonNull;
import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+
import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import java.util.SortedMap;
/**
* Manages Trickplay storage.
*/
public class TrickplayStorageManager implements BufferManager.StorageManager {
+ // TODO: Support multi-sessions.
private static final String BUFFER_DIR = "timeshift";
// Copied from android.provider.Settings.Global (hidden fields)
@@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024;
- private final File mBufferDir;
+ private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask;
+ private static File sBufferDir;
+ private static long sStorageBufferBytes;
+
private final long mMaxBufferSize;
- private final long mStorageBufferBytes;
- private static long getStorageBufferBytes(Context context, File path) {
+ private static void initParamsIfNeeded(Context context, @NonNull File path) {
+ // TODO: Support multi-sessions.
+ SoftPreconditions.checkState(
+ sBufferDir == null || sBufferDir.equals(path));
+ if (path.equals(sBufferDir)) {
+ return;
+ }
+ sBufferDir = path;
long lowPercentage = Settings.Global.getInt(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
- long lowBytes = path.getTotalSpace() * lowPercentage / 100;
+ long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100;
long maxLowBytes = Settings.Global.getLong(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
- return Math.min(lowBytes, maxLowBytes);
+ sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes);
}
- public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) {
- mBufferDir = new File(baseDir, BUFFER_DIR);
- mBufferDir.mkdirs();
+ public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) {
+ initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR));
+ sBufferDir.mkdirs();
mMaxBufferSize = maxBufferSize;
clearStorage();
- mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir);
}
- @Override
- public void clearStorage() {
- File files[] = mBufferDir.listFiles();
- if (files == null || files.length == 0) {
- return;
+ private void clearStorage() {
+ long now = System.currentTimeMillis();
+ if (sLastCacheCleanUpTask != null) {
+ sLastCacheCleanUpTask.cancel(true);
}
- if (Looper.myLooper() == Looper.getMainLooper()) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- for (File file : files) {
+ sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+ File files[] = sBufferDir.listFiles();
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for (File file : files) {
+ if (isCancelled()) {
+ break;
+ }
+ long lastModified = file.lastModified();
+ if (lastModified != 0 && lastModified < now) {
file.delete();
}
- return null;
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- for (File file : files) {
- file.delete();
+ return null;
}
- }
+ };
+ sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public File getBufferDir() {
- return mBufferDir;
+ return sBufferDir;
}
@Override
@@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public boolean hasEnoughBuffer(long pendingDelete) {
- return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes;
+ return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes;
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) {
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
return null;
}
@Override
- public ArrayList<Long> readIndexFile(String trackId) {
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) {
return null;
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) {
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) {
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) {
+ public void writeIndexFile(String trackName,
+ SortedMap<Long, Pair<SampleChunk, Integer>> index) {
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java
new file mode 100644
index 00000000..356636cc
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java
@@ -0,0 +1,249 @@
+/*
+ * 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.exoplayer.ffmpeg;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.support.annotation.VisibleForTesting;
+import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.Features;
+import com.android.tv.tuner.exoplayer.audio.AudioDecoder;
+
+import java.nio.ByteBuffer;
+
+/**
+ * The class connects {@link FfmpegDecoderService} to decode audio samples.
+ * In order to sandbox ffmpeg based decoder, {@link FfmpegDecoderService} is an isolated process
+ * without any permission and connected by binder.
+ */
+public class FfmpegDecoderClient extends AudioDecoder {
+ private static FfmpegDecoderClient sInstance;
+
+ private IFfmpegDecoder mService;
+ private Boolean mIsAvailable;
+
+ private static final String FFMPEG_DECODER_SERVICE_FILTER =
+ "com.android.tv.tuner.exoplayer.ffmpeg.IFfmpegDecoder";
+ private static final long FFMPEG_SERVICE_CONNECT_TIMEOUT_MS = 500;
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mService = IFfmpegDecoder.Stub.asInterface(service);
+ synchronized (FfmpegDecoderClient.this) {
+ try {
+ mIsAvailable = mService.isAvailable();
+ } catch (RemoteException e) {
+ }
+ FfmpegDecoderClient.this.notify();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ synchronized (FfmpegDecoderClient.this) {
+ sInstance.releaseLocked();
+ mIsAvailable = false;
+ mService = null;
+ }
+ }
+ };
+
+ /**
+ * Connects to the decoder service for future uses.
+ * @param context
+ * @return {@code true} when decoder service is connected.
+ */
+ @MainThread
+ public synchronized static boolean connect(Context context) {
+ if (Features.AC3_SOFTWARE_DECODE.isEnabled(context)) {
+ if (sInstance == null) {
+ sInstance = new FfmpegDecoderClient();
+ Intent intent =
+ new Intent(FFMPEG_DECODER_SERVICE_FILTER)
+ .setComponent(
+ new ComponentName(context, FfmpegDecoderService.class));
+ if (context.bindService(intent, sInstance.mConnection, Context.BIND_AUTO_CREATE)) {
+ return true;
+ } else {
+ sInstance = null;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Disconnects from the decoder service and release resources.
+ * @param context
+ */
+ @MainThread
+ public synchronized static void disconnect(Context context) {
+ if (sInstance != null) {
+ synchronized (sInstance) {
+ sInstance.releaseLocked();
+ if (sInstance.mIsAvailable != null && sInstance.mIsAvailable) {
+ context.unbindService(sInstance.mConnection);
+ }
+ sInstance.mIsAvailable = false;
+ sInstance.mService = null;
+ }
+ sInstance = null;
+ }
+ }
+
+ /**
+ * Returns whether service is available or not.
+ * Before using client, this should be used to check availability.
+ */
+ @WorkerThread
+ public synchronized static boolean isAvailable() {
+ if (sInstance != null) {
+ return sInstance.available();
+ }
+ return false;
+ }
+
+ /**
+ * Returns an client instance.
+ */
+ public synchronized static FfmpegDecoderClient getInstance() {
+ if (sInstance != null) {
+ sInstance.createDecoder();
+ }
+ return sInstance;
+ }
+
+ private FfmpegDecoderClient() {
+ }
+
+ private synchronized boolean available() {
+ if (mIsAvailable == null) {
+ try {
+ this.wait(FFMPEG_SERVICE_CONNECT_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ }
+ }
+ return mIsAvailable != null && mIsAvailable == true;
+ }
+
+ private synchronized void createDecoder() {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return;
+ }
+ try {
+ mService.create();
+ } catch (RemoteException e) {
+ }
+ }
+
+ private void releaseLocked() {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return;
+ }
+ try {
+ mService.release();
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public synchronized void release() {
+ releaseLocked();
+ }
+
+ @Override
+ public synchronized void decode(SampleHolder sampleHolder) {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return;
+ }
+ byte[] sampleBytes = new byte [sampleHolder.data.limit()];
+ sampleHolder.data.get(sampleBytes, 0, sampleBytes.length);
+ try {
+ mService.decode(sampleHolder.timeUs, sampleBytes);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public synchronized void resetDecoderState(String mimeType) {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return;
+ }
+ try {
+ mService.resetDecoderState(mimeType);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @Override
+ public synchronized ByteBuffer getDecodedSample() {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return null;
+ }
+ try {
+ byte[] outputBytes = mService.getDecodedSample();
+ if (outputBytes != null && outputBytes.length > 0) {
+ return ByteBuffer.wrap(outputBytes);
+ }
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ @Override
+ public synchronized long getDecodedTimeUs() {
+ if (mIsAvailable == null || mIsAvailable == false) {
+ return 0;
+ }
+ try {
+ return mService.getDecodedTimeUs();
+ } catch (RemoteException e) {
+ }
+ return 0;
+ }
+
+ @VisibleForTesting
+ public boolean testSandboxIsolatedProcess() {
+ // When testing isolated process, we will check the permission in FfmpegDecoderService.
+ // If the service have any permission, an exception will be thrown.
+ try {
+ mService.testSandboxIsolatedProcess();
+ } catch (RemoteException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ public void testSandboxMinijail() {
+ // When testing minijail, we will call a system call which is blocked by minijail. In that
+ // case, the FfmpegDecoderService will be disconnected, we can check the connection status
+ // to make sure if the minijail works or not.
+ try {
+ mService.testSandboxMinijail();
+ } catch (RemoteException e) {
+ }
+ }
+}
diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java
new file mode 100644
index 00000000..3ebdd381
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java
@@ -0,0 +1,205 @@
+/*
+ * 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.exoplayer.ffmpeg;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
+import android.os.AsyncTask;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioDecoder;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Ffmpeg based audio decoder service.
+ * It should be isolatedProcess due to security reason.
+ */
+public class FfmpegDecoderService extends Service {
+ private static final String TAG = "FfmpegDecoderService";
+ private static final boolean DEBUG = false;
+
+ private static final String POLICY_FILE = "whitelist.policy";
+
+ private static final long MINIJAIL_SETUP_WAIT_TIMEOUT_MS = 5000;
+
+ private static boolean sLibraryLoaded = true;
+
+ static {
+ try {
+ System.loadLibrary("minijail_jni");
+ } catch (Exception | Error e) {
+ Log.e(TAG, "Load minijail failed:", e);
+ sLibraryLoaded = false;
+ }
+ }
+
+ private FfmpegDecoder mBinder = new FfmpegDecoder();
+ private volatile Object mMinijailSetupMonitor = new Object();
+ //@GuardedBy("mMinijailSetupMonitor")
+ private volatile Boolean mMinijailSetup;
+
+ @Override
+ public void onCreate() {
+ if (sLibraryLoaded) {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (mMinijailSetupMonitor) {
+ int pipeFd = getPolicyPipeFd();
+ if (pipeFd <= 0) {
+ Log.e(TAG, "fail to open policy file");
+ mMinijailSetup = false;
+ } else {
+ nativeSetupMinijail(pipeFd);
+ mMinijailSetup = true;
+ if (DEBUG) Log.d(TAG, "Minijail setup successfully");
+ }
+ mMinijailSetupMonitor.notify();
+ }
+ return null;
+ }
+ }.execute();
+ } else {
+ synchronized (mMinijailSetupMonitor) {
+ mMinijailSetup = false;
+ mMinijailSetupMonitor.notify();
+ }
+ }
+ super.onCreate();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private int getPolicyPipeFd() {
+ try {
+ ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
+ new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);
+ final AssetFileDescriptor policyFile = getAssets().openFd("whitelist.policy");
+ final byte[] buffer = new byte[2048];
+ final FileInputStream policyStream = policyFile.createInputStream();
+ while (true) {
+ int bytesRead = policyStream.read(buffer);
+ if (bytesRead == -1) break;
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ policyStream.close();
+ outputStream.close();
+ return pipe[0].detachFd();
+ } catch (IOException e) {
+ Log.e(TAG, "Policy file not found:" + e);
+ }
+ return -1;
+ }
+
+ private final class FfmpegDecoder extends IFfmpegDecoder.Stub {
+ FfmpegAudioDecoder mDecoder;
+ @Override
+ public boolean isAvailable() {
+ return isMinijailSetupDone() && FfmpegAudioDecoder.isAvailable();
+ }
+
+ @Override
+ public void create() {
+ mDecoder = new FfmpegAudioDecoder(FfmpegDecoderService.this);
+ }
+
+ @Override
+ public void release() {
+ if (mDecoder != null) {
+ mDecoder.release();
+ mDecoder = null;
+ }
+ }
+
+ @Override
+ public void decode(long timeUs, byte[] sample) {
+ if (!isMinijailSetupDone()) {
+ // If minijail is not setup, we don't run decode for better security.
+ return;
+ }
+ mDecoder.decode(timeUs, sample);
+ }
+
+ @Override
+ public void resetDecoderState(String mimetype) {
+ mDecoder.resetDecoderState(mimetype);
+ }
+
+ @Override
+ public byte[] getDecodedSample() {
+ ByteBuffer decodedBuffer = mDecoder.getDecodedSample();
+ byte[] ret = new byte[decodedBuffer.limit()];
+ decodedBuffer.get(ret, 0, ret.length);
+ return ret;
+ }
+
+ @Override
+ public long getDecodedTimeUs() {
+ return mDecoder.getDecodedTimeUs();
+ }
+
+ private boolean isMinijailSetupDone() {
+ synchronized (mMinijailSetupMonitor) {
+ if (DEBUG) Log.d(TAG, "mMinijailSetup in isAvailable(): " + mMinijailSetup);
+ if (mMinijailSetup == null) {
+ try {
+ if (DEBUG) Log.d(TAG, "Wait till Minijail setup is done");
+ mMinijailSetupMonitor.wait(MINIJAIL_SETUP_WAIT_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return mMinijailSetup != null && mMinijailSetup;
+ }
+ }
+
+ @Override
+ public void testSandboxIsolatedProcess() {
+ if (!isMinijailSetupDone()) {
+ // If minijail is not setup, we return directly to make the test fail.
+ return;
+ }
+ if (FfmpegDecoderService.this.checkSelfPermission("android.permission.INTERNET")
+ == PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Shouldn't have the permission of internet");
+ }
+ }
+
+ @Override
+ public void testSandboxMinijail() {
+ if (!isMinijailSetupDone()) {
+ // If minijail is not setup, we return directly to make the test fail.
+ return;
+ }
+ nativeTestMinijail();
+ }
+ }
+
+ private native void nativeSetupMinijail(int policyFd);
+ private native void nativeTestMinijail();
+}
diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl b/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl
new file mode 100644
index 00000000..ed053790
--- /dev/null
+++ b/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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.exoplayer.ffmpeg;
+
+interface IFfmpegDecoder {
+ boolean isAvailable();
+ void create();
+ void release();
+ void resetDecoderState(String mimetype);
+ void decode(long timeUs, in byte[] sample);
+ byte[] getDecodedSample();
+ long getDecodedTimeUs();
+ void testSandboxIsolatedProcess();
+ void testSandboxMinijail();
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
index 97d9ece3..e0e21a20 100644
--- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
+++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
@@ -21,6 +21,7 @@ import android.support.annotation.NonNull;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
+import com.android.tv.common.BuildConfig;
import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.tuner.R;
@@ -36,6 +37,24 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment {
"com.android.tv.tuner.setup.ConnectionTypeFragment";
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ ((TunerSetupActivity) getActivity()).clearTunerHal();
+ super.onDestroy();
+ }
+
+ @Override
protected SetupGuidedStepFragment onCreateContentFragment() {
return new ContentFragment();
}
diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
new file mode 100644
index 00000000..025b9193
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -0,0 +1,178 @@
+/*
+ * 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.InputFilter.AllCaps;
+import android.view.View;
+import android.widget.TextView;
+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 com.android.tv.util.LocationUtils;
+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();
+ String region = LocationUtils.getCurrentCountry(getContext());
+ if (postalCode != null && PostalCodeUtils.matches(postalCode, region)) {
+ 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) {
+ int maxLength = PostalCodeUtils.getRegionMaxLength(getContext());
+ mEditedActionView = getView().findViewById(R.id.guidedactions_editable);
+ ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title))
+ .setFilters(
+ new InputFilter[] {
+ new InputFilter.LengthFilter(maxLength), new AllCaps()
+ });
+ }
+ mEditedActionView.performClick();
+ }
+ }
+ }
+
+ @Override
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ mProceed = true;
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.postal_code_guidance_title);
+ String description = getString(R.string.postal_code_guidance_description);
+ String breadcrumb = getString(R.string.ut_setup_breadcrumb);
+ return new Guidance(title, description, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ String description = getString(R.string.postal_code_action_description);
+ mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true)
+ .description(description).build();
+ actions.add(mEditAction);
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist() {
+ @Override
+ public int getItemViewType(GuidedAction action) {
+ if (action.isEditable()) {
+ return VIEW_TYPE_EDITABLE;
+ }
+ return super.getItemViewType(action);
+ }
+
+ @Override
+ public int onProvideItemLayoutId(int viewType) {
+ if (viewType == VIEW_TYPE_EDITABLE) {
+ return R.layout.guided_action_editable;
+ }
+ return super.onProvideItemLayoutId(viewType);
+ }
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java
index 3b61debb..b6936e38 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,25 +36,21 @@ 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.PsipData;
import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
import com.android.tv.tuner.source.FileTsStreamer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsStreamer;
import com.android.tv.tuner.source.TunerTsStreamer;
import com.android.tv.tuner.tvinput.ChannelDataManager;
import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
-
-import junit.framework.Assert;
import java.util.ArrayList;
import java.util.List;
@@ -67,6 +64,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 +96,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 +119,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 +152,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 +175,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,13 +264,13 @@ 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");
}
mScanTsStreamer = new TunerTsStreamer(hal, this);
}
- mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this) : null;
+ mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null;
mConditionStopped = new ConditionVariable();
mChannelDataManager.setChannelScanListener(this, new Handler());
}
@@ -316,10 +325,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();
}
@@ -340,8 +356,8 @@ public class ScanFragment extends SetupFragment {
Log.i(TAG, "Tuning to " + frequency + " " + modulation);
TsStreamer streamer = getStreamer(scanChannel.type);
- Assert.assertNotNull(streamer);
- if (streamer.startStream(scanChannel)) {
+ SoftPreconditions.checkNotNull(streamer);
+ if (streamer != null && streamer.startStream(scanChannel)) {
mLatch = new CountDownLatch(1);
try {
mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS);
@@ -360,11 +376,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)) {
@@ -427,8 +439,8 @@ public class ScanFragment extends SetupFragment {
// Playbacks with video-only stream have not been tested yet.
// No video-only channel has been found.
addChannel(channel);
+ mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
}
- mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
}
public void showFinishingProgressDialog() {
@@ -446,15 +458,21 @@ public class ScanFragment extends SetupFragment {
mIsFinished = true;
TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(),
mChannelDataManager.getScannedChannelCount());
- // Cancel a previously shown recommendation card.
- TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext());
+ // Cancel a previously shown notification.
+ TunerSetupActivity.cancelNotification(mActivity.getApplicationContext());
// Mark scan as done
TunerPreferences.setScanDone(mActivity.getApplicationContext());
// finishing will be done manually.
if (mFinishingProgressDialog != null) {
mFinishingProgressDialog.dismiss();
}
- onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ // 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..e9f3baa7 100644
--- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java
+++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
@@ -19,6 +19,7 @@ package com.android.tv.tuner.setup;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.Notification;
+import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
@@ -29,49 +30,98 @@ 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.Build;
import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
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.Features;
import com.android.tv.TvApplication;
+import com.android.tv.common.AutoCloseableUtils;
+import com.android.tv.common.SoftPreconditions;
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 java.util.concurrent.Executor;
/**
* An activity that serves tuner setup process.
*/
public class TunerSetupActivity extends SetupActivity {
- private final String TAG = "TunerSetupActivity";
- // For the recommendation card
+ private static final String 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 notification.
private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity";
+ private static final String TUNER_SET_UP_NOTIFICATION_CHANNEL_ID = "tuner_setup_channel";
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,
- R.raw.ut_us_cable_standard_center_frequencies_qam256,
- R.raw.ut_us_all,
- R.raw.ut_kr_atsc_center_frequencies_8vsb,
- R.raw.ut_kr_cable_standard_center_frequencies_qam256,
- R.raw.ut_kr_all,
- R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256};
+ R.raw.ut_us_atsc_center_frequencies_8vsb,
+ R.raw.ut_us_cable_standard_center_frequencies_qam256,
+ R.raw.ut_us_all,
+ R.raw.ut_kr_atsc_center_frequencies_8vsb,
+ R.raw.ut_kr_cable_standard_center_frequencies_qam256,
+ R.raw.ut_kr_all,
+ R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256,
+ R.raw.ut_euro_dvbt_all,
+ R.raw.ut_euro_dvbt_all,
+ R.raw.ut_euro_dvbt_all
+ };
private ScanFragment mLastScanFragment;
+ private Integer mTunerType;
+ private TunerHalFactory mTunerHalFactory;
+ private boolean mNeedToShowPostalCodeFragment;
+ private String mPreviousPostalCode;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
+ new AsyncTask<Void, Void, Integer>() {
+ @Override
+ protected Integer doInBackground(Void... arg0) {
+ return TunerHal.getTunerTypeAndCount(TunerSetupActivity.this).first;
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (!TunerSetupActivity.this.isDestroyed()) {
+ mTunerType = result;
+ if (result == null) {
+ finish();
+ } else {
+ showInitialFragment();
+ }
+ }
+ }
+ }.execute();
TvApplication.setCurrentRunningProcess(this, false);
super.onCreate(savedInstanceState);
// TODO: check {@link shouldShowRequestPermissionRationale}.
@@ -79,16 +129,52 @@ 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);
+ }
+ mTunerHalFactory = new TunerHalFactory(getApplicationContext());
+ try {
+ // Updating postal code takes time, therefore we called it here for "warm-up".
+ mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this);
+ 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();
- fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION
- | SetupFragment.FRAGMENT_REENTER_TRANSITION);
- return fragment;
+ if (mTunerType != null) {
+ 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;
+ } else {
+ return null;
+ }
}
@Override
@@ -101,34 +187,42 @@ public class TunerSetupActivity extends SetupActivity {
setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK);
finish();
break;
- default: {
- SetupFragment fragment = new ConnectionTypeFragment();
- fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
- | SetupFragment.FRAGMENT_RETURN_TRANSITION);
- showFragment(fragment, true);
+ default:
+ if (mNeedToShowPostalCodeFragment
+ || Features.ENABLE_CLOUD_EPG_REGION.isEnabled(
+ 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.getOrCreate() == 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 +231,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);
@@ -183,6 +281,14 @@ public class TunerSetupActivity extends SetupActivity {
return super.onKeyUp(keyCode, event);
}
+ @Override
+ public void onDestroy() {
+ if (mPreviousPostalCode != null && PostalCodeUtils.getLastPostalCode(this) == null) {
+ PostalCodeUtils.setLastPostalCode(this, mPreviousPostalCode);
+ }
+ super.onDestroy();
+ }
+
/**
* A callback to be invoked when the TvInputService is enabled or disabled.
*
@@ -190,17 +296,17 @@ public class TunerSetupActivity extends SetupActivity {
* @param enabled {@code true} for the {@link TunerTvInputService} to be enabled;
* otherwise {@code false}
*/
- public static void onTvInputEnabled(Context context, boolean enabled) {
- // Send a recommendation card for tuner setup if there's no channels and the tuner TV input
+ public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) {
+ // Send a notification for tuner setup if there's no channels and the tuner TV input
// setup has been not done.
boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context);
int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context);
if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) {
TunerPreferences.setShouldShowSetupActivity(context, true);
- sendRecommendationCard(context);
+ sendNotification(context, tunerType);
} else {
TunerPreferences.setShouldShowSetupActivity(context, false);
- cancelRecommendationCard(context);
+ cancelNotification(context);
}
}
@@ -213,7 +319,7 @@ public class TunerSetupActivity extends SetupActivity {
String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(),
TunerTvInputService.class.getName()));
- // Make an intent to launch the setup activity of USB tuner TV input.
+ // Make an intent to launch the setup activity of TV tuner input.
Intent intent = TvCommonUtils.createSetupIntent(
new Intent(context, TunerSetupActivity.class), inputId);
intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId);
@@ -224,6 +330,27 @@ public class TunerSetupActivity extends SetupActivity {
}
/**
+ * Gets the currently used tuner HAL.
+ */
+ TunerHal getTunerHal() {
+ return mTunerHalFactory.getOrCreate();
+ }
+
+ /**
+ * Generates tuner HAL.
+ */
+ void generateTunerHal() {
+ mTunerHalFactory.generate();
+ }
+
+ /**
+ * Clears the currently used tuner HAL.
+ */
+ void clearTunerHal() {
+ mTunerHalFactory.clear();
+ }
+
+ /**
* Returns a {@link PendingIntent} to launch the tuner TV input service.
*
* @param context a {@link Context} instance
@@ -233,34 +360,53 @@ public class TunerSetupActivity extends SetupActivity {
PendingIntent.FLAG_UPDATE_CURRENT);
}
+ private static void sendNotification(Context context, Integer tunerType) {
+ SoftPreconditions.checkState(tunerType != null, TAG,
+ "tunerType is null when send notification");
+ if (tunerType == null) {
+ return;
+ }
+ Resources resources = context.getResources();
+ String contentTitle = resources.getString(R.string.ut_setup_notification_content_title);
+ int contentTextId = 0;
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ contentTextId = R.string.bt_setup_notification_content_text;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ contentTextId = R.string.ut_setup_notification_content_text;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ contentTextId = R.string.nt_setup_notification_content_text;
+ break;
+ }
+ String contentText = resources.getString(contentTextId);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ sendNotificationInternal(context, contentTitle, contentText);
+ } else {
+ Bitmap largeIcon = BitmapFactory.decodeResource(resources,
+ R.drawable.recommendation_antenna);
+ sendRecommendationCard(context, contentTitle, contentText, largeIcon);
+ }
+ }
+
/**
* Sends the recommendation card to start the tuner TV input setup activity.
*
* @param context a {@link Context} instance
*/
- private static void sendRecommendationCard(Context context) {
- 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);
- }
- Bitmap largeIcon = BitmapFactory.decodeResource(resources,
- R.drawable.recommendation_antenna);
-
+ private static void sendRecommendationCard(Context context, String contentTitle,
+ String contentText, Bitmap largeIcon) {
// Build and send the notification.
Notification notification = new NotificationCompat.BigPictureStyle(
new NotificationCompat.Builder(context)
.setAutoCancel(false)
- .setContentTitle(focusedTitle)
- .setContentText(title)
- .setContentInfo(title)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setContentInfo(contentText)
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setLargeIcon(largeIcon)
- .setSmallIcon(resources.getIdentifier(
+ .setSmallIcon(context.getResources().getIdentifier(
TAG_ICON, TAG_DRAWABLE, context.getPackageName()))
.setContentIntent(createPendingIntentForSetupActivity(context)))
.build();
@@ -269,14 +415,129 @@ public class TunerSetupActivity extends SetupActivity {
notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification);
}
+ private static void sendNotificationInternal(Context context, String contentTitle,
+ String contentText) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ notificationManager.createNotificationChannel(new NotificationChannel(
+ TUNER_SET_UP_NOTIFICATION_CHANNEL_ID,
+ context.getResources().getString(R.string.ut_setup_notification_channel_name),
+ NotificationManager.IMPORTANCE_HIGH));
+ Notification notification = new Notification.Builder(
+ context, TUNER_SET_UP_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(context.getResources().getIdentifier(
+ TAG_ICON, TAG_DRAWABLE, context.getPackageName()))
+ .setContentIntent(createPendingIntentForSetupActivity(context))
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .extend(new Notification.TvExtender())
+ .build();
+ 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.
+ * Cancels the previously shown notification.
*
* @param context a {@link Context} instance
*/
- public static void cancelRecommendationCard(Context context) {
+ public static void cancelNotification(Context context) {
NotificationManager notificationManager = (NotificationManager) context
.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.
+ */
+ @WorkerThread
+ TunerHal getOrCreate() {
+ 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.
+ */
+ @MainThread
+ void generate() {
+ if (mGenerateTunerHalTask == null && mTunerHal == null) {
+ mGenerateTunerHalTask = new GenerateTunerHalTask();
+ mGenerateTunerHalTask.executeOnExecutor(mExecutor);
+ }
+ }
+
+ /**
+ * Clears the currently used tuner hal.
+ */
+ @MainThread
+ void clear() {
+ if (mGenerateTunerHalTask != null) {
+ mGenerateTunerHalTask.cancel(true);
+ mGenerateTunerHalTask = null;
+ }
+ if (mTunerHal != null) {
+ AutoCloseableUtils.closeQuietly(mTunerHal);
+ mTunerHal = null;
+ }
+ }
+
+ @WorkerThread
+ protected TunerHal createInstance() {
+ return TunerHal.createInstance(mContext);
+ }
+
+ class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
+ @Override
+ protected TunerHal doInBackground(Void... args) {
+ return createInstance();
+ }
+
+ @Override
+ protected void onPostExecute(TunerHal tunerHal) {
+ mTunerHal = tunerHal;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 7e809411..feae1ec9 100644
--- a/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -18,18 +18,14 @@ package com.android.tv.tuner.setup;
import android.os.Bundle;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
import android.support.v17.leanback.widget.GuidedAction;
-import android.view.LayoutInflater;
-import android.view.View;
-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;
-
import java.util.List;
/**
@@ -41,7 +37,9 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
@Override
protected SetupGuidedStepFragment onCreateContentFragment() {
- return new ContentFragment();
+ ContentFragment fragment = new ContentFragment();
+ fragment.setArguments(getArguments());
+ return fragment;
}
@Override
@@ -58,11 +56,10 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
private int mChannelCountOnPreference;
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mChannelCountOnPreference = TunerPreferences
- .getScannedChannelCount(getActivity().getApplicationContext());
- return super.onCreateView(inflater, container, savedInstanceState);
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ mChannelCountOnPreference =
+ TunerPreferences.getScannedChannelCount(getActivity().getApplicationContext());
+ super.onCreate(savedInstanceState);
}
@NonNull
@@ -70,20 +67,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..f17dd46b 100644
--- a/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -16,12 +16,14 @@
package com.android.tv.tuner.source;
+import android.content.Context;
import android.os.Environment;
import android.util.Log;
import android.util.SparseBooleanArray;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
+import com.android.tv.Features;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.ChannelScanFileParser.ScanChannel;
import com.android.tv.tuner.data.TunerChannel;
@@ -60,6 +62,7 @@ public class FileTsStreamer implements TsStreamer {
private final Object mCircularBufferMonitor = new Object();
private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE];
private final FileSourceEventDetector mEventDetector;
+ private final Context mContext;
private long mBytesFetched;
private long mLastReadPosition;
@@ -120,8 +123,11 @@ public class FileTsStreamer implements TsStreamer {
* Creates {@link TsStreamer} for scanning & playing MPEG-2 TS file.
* @param eventListener the listener for channel & program information
*/
- public FileTsStreamer(EventDetector.EventListener eventListener) {
- mEventDetector = new FileSourceEventDetector(eventListener);
+ public FileTsStreamer(EventDetector.EventListener eventListener, Context context) {
+ mEventDetector =
+ new FileSourceEventDetector(
+ eventListener, Features.ENABLE_FILE_DVB.isEnabled(context));
+ mContext = context;
}
@Override
@@ -132,8 +138,12 @@ public class FileTsStreamer implements TsStreamer {
return false;
}
mEventDetector.start(mSource, FileSourceEventDetector.ALL_PROGRAM_NUMBERS);
- mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID);
mSource.addPidFilter(TsParser.PAT_PID);
+ mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID);
+ if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) {
+ mSource.addPidFilter(TsParser.DVB_EIT_PID);
+ mSource.addPidFilter(TsParser.DVB_SDT_PID);
+ }
synchronized (mCircularBufferMonitor) {
if (mStreaming) {
return true;
@@ -160,8 +170,12 @@ public class FileTsStreamer implements TsStreamer {
mSource.addPidFilter(i);
}
mSource.addPidFilter(channel.getPcrPid());
- mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID);
mSource.addPidFilter(TsParser.PAT_PID);
+ mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID);
+ if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) {
+ mSource.addPidFilter(TsParser.DVB_EIT_PID);
+ mSource.addPidFilter(TsParser.DVB_SDT_PID);
+ }
synchronized (mCircularBufferMonitor) {
if (mStreaming) {
return true;
@@ -256,7 +270,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..16be7582 100644
--- a/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -17,9 +17,11 @@
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.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
import com.android.tv.tuner.tvinput.EventDetector;
import java.util.Map;
@@ -31,8 +33,6 @@ import java.util.concurrent.ConcurrentHashMap;
* One TsDataSourceManager should be created for per session.
*/
public class TsDataSourceManager {
- private static String TAG = "TsDataSourceManager";
-
private static final Object sLock = new Object();
private static final Map<TsDataSource, TsStreamer> sTsStreamers =
new ConcurrentHashMap<>();
@@ -80,7 +80,7 @@ public class TsDataSourceManager {
if (mIsRecording) {
return null;
}
- FileTsStreamer streamer = new FileTsStreamer(eventListener);
+ FileTsStreamer streamer = new FileTsStreamer(eventListener, context);
if (streamer.startStream(channel)) {
TsDataSource source = streamer.createDataSource();
sTsStreamers.put(source, streamer);
@@ -127,6 +127,14 @@ public class TsDataSourceManager {
}
/**
+ * Add tuner hal into TunerTsStreamerManager for test.
+ */
+ @VisibleForTesting
+ public void addTunerHalForTest(TunerHal tunerHal) {
+ mTunerStreamerManager.addTunerHal(tunerHal, mId);
+ }
+
+ /**
* Releases persistent resources.
*/
public void release() {
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java
index b24048e6..843cbdb7 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -18,6 +18,7 @@ package com.android.tv.tuner.source;
import android.content.Context;
import android.util.Log;
+import android.util.Pair;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
@@ -30,6 +31,7 @@ import com.android.tv.tuner.tvinput.EventDetector;
import com.android.tv.tuner.tvinput.EventDetector.EventListener;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
@@ -42,23 +44,27 @@ 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;
private TunerChannel mChannel;
private Thread mStreamingThread;
private final EventDetector mEventDetector;
+ private final List<Pair<EventListener, Boolean>> mEventListenerActions = new ArrayList<>();
private final TsStreamWriter mTsStreamWriter;
+ private String mChannelNumber;
public static class TunerDataSource extends TsDataSource {
private final TunerTsStreamer mTsStreamer;
@@ -103,6 +109,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 +129,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 +143,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 +167,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 +176,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
if (mTsStreamWriter != null) {
mTsStreamWriter.setChannel(mChannel);
@@ -172,7 +191,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 +202,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
mStreamingThread = new StreamingThread();
mStreamingThread.start();
@@ -258,6 +276,26 @@ public class TunerTsStreamer implements TsStreamer {
}
}
+ public String getStreamerInfo() {
+ return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming;
+ }
+
+ public void registerListener(EventListener listener) {
+ if (mEventDetector != null && listener != null) {
+ synchronized (mEventListenerActions) {
+ mEventListenerActions.add(new Pair<>(listener, true));
+ }
+ }
+ }
+
+ public void unregisterListener(EventListener listener) {
+ if (mEventDetector != null) {
+ synchronized (mEventListenerActions) {
+ mEventListenerActions.add(new Pair(listener, false));
+ }
+ }
+ }
+
private class StreamingThread extends Thread {
@Override
public void run() {
@@ -271,6 +309,20 @@ public class TunerTsStreamer implements TsStreamer {
}
}
+ if (mEventDetector != null) {
+ synchronized (mEventListenerActions) {
+ for (Pair listenerAction : mEventListenerActions) {
+ EventListener listener = (EventListener) listenerAction.first;
+ if ((boolean) listenerAction.second) {
+ mEventDetector.registerListener(listener);
+ } else {
+ mEventDetector.unregisterListener(listener);
+ }
+ }
+ mEventListenerActions.clear();
+ }
+ }
+
int bytesWritten = mTunerHal.readTsStream(dataBuffer, dataBuffer.length);
if (bytesWritten <= 0) {
try {
@@ -321,21 +373,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..258a4d86 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -42,6 +42,7 @@ class TunerTsStreamerManager {
private final Object mCancelLock = new Object();
private final StreamerFinder mStreamerFinder = new StreamerFinder();
private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
+ private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>();
private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
private final TunerHalManager mTunerHalManager = new TunerHalManager();
private static TunerTsStreamerManager sInstance;
@@ -68,6 +69,8 @@ class TunerTsStreamerManager {
mStreamerFinder.appendSessionLocked(channel, sessionId);
TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ streamer.registerListener(listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -83,6 +86,7 @@ class TunerTsStreamerManager {
if (!creator.isCancelledLocked()) {
mStreamerFinder.putLocked(channel, sessionId, streamer);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -104,6 +108,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 +131,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 +274,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 +296,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..e1f890f3 100644
--- a/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/src/com/android/tv/tuner/ts/SectionParser.java
@@ -18,11 +18,12 @@ package com.android.tv.tuner.ts;
import android.media.tv.TvContentRating;
import android.media.tv.TvContract.Programs.Genres;
+import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
-import com.android.tv.tuner.data.nano.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;
@@ -34,24 +35,32 @@ import com.android.tv.tuner.data.PsipData.ExtendedChannelNameDescriptor;
import com.android.tv.tuner.data.PsipData.GenreDescriptor;
import com.android.tv.tuner.data.PsipData.Iso639LanguageDescriptor;
import com.android.tv.tuner.data.PsipData.MgtItem;
+import com.android.tv.tuner.data.PsipData.ParentalRatingDescriptor;
import com.android.tv.tuner.data.PsipData.PsipSection;
import com.android.tv.tuner.data.PsipData.RatingRegion;
import com.android.tv.tuner.data.PsipData.RegionalRating;
+import com.android.tv.tuner.data.PsipData.SdtItem;
+import com.android.tv.tuner.data.PsipData.ServiceDescriptor;
+import com.android.tv.tuner.data.PsipData.ShortEventDescriptor;
import com.android.tv.tuner.data.PsipData.TsDescriptor;
import com.android.tv.tuner.data.PsipData.VctItem;
+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.util.ByteArrayBuffer;
+import com.android.tv.tuner.util.ConvertUtils;
import com.ibm.icu.text.UnicodeDecompressor;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
+import java.util.Calendar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Parses ATSC PSIP sections.
@@ -68,6 +77,13 @@ public class SectionParser {
private static final byte TABLE_ID_EIT = (byte) 0xcb;
private static final byte TABLE_ID_ETT = (byte) 0xcc;
+ // Table id for DVB
+ private static final byte TABLE_ID_SDT = (byte) 0x42;
+ private static final byte TABLE_ID_DVB_ACTUAL_P_F_EIT = (byte) 0x4e;
+ private static final byte TABLE_ID_DVB_OTHER_P_F_EIT = (byte) 0x4f;
+ private static final byte TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT = (byte) 0x50;
+ private static final byte TABLE_ID_DVB_OTHER_SCHEDULE_EIT = (byte) 0x60;
+
// For details of the structure for the tags of descriptors, see ATSC A/65 Table 6.25.
public static final int DESCRIPTOR_TAG_ISO639LANGUAGE = 0x0a;
public static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86;
@@ -76,6 +92,12 @@ public class SectionParser {
public static final int DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = 0xa0;
public static final int DESCRIPTOR_TAG_GENRE = 0xab;
+ // For details of the structure for the tags of DVB descriptors, see DVB Document A038 Table 12.
+ public static final int DVB_DESCRIPTOR_TAG_SERVICE = 0x48;
+ public static final int DVB_DESCRIPTOR_TAG_SHORT_EVENT = 0X4d;
+ public static final int DVB_DESCRIPTOR_TAG_CONTENT = 0x54;
+ public static final int DVB_DESCRIPTOR_TAG_PARENTAL_RATING = 0x55;
+
private static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00;
private static final byte MODE_SELECTED_UNICODE_RANGE_1 = (byte) 0x00; // 0x0000 - 0x00ff
private static final byte MODE_UTF16 = (byte) 0x3f;
@@ -88,17 +110,57 @@ public class SectionParser {
// The following values are defined in the live channels app.
// See https://developer.android.com/reference/android/media/tv/TvContentRating.html.
+ private static final String RATING_DOMAIN = "com.android.tv";
private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV";
+ private static final String RATING_REGION_RATING_SYSTEM_US_MV = "US_MV";
private static final String RATING_REGION_RATING_SYSTEM_KR_TV = "KR_TV";
private static final String[] RATING_REGION_TABLE_US_TV = {
"US_TV_Y", "US_TV_Y7", "US_TV_G", "US_TV_PG", "US_TV_14", "US_TV_MA"
};
+ private static final String[] RATING_REGION_TABLE_US_MV = {
+ "US_MV_G", "US_MV_PG", "US_MV_PG13", "US_MV_R", "US_MV_NC17"
+ };
+
private static final String[] RATING_REGION_TABLE_KR_TV = {
"KR_TV_ALL", "KR_TV_7", "KR_TV_12", "KR_TV_15", "KR_TV_19"
};
+ private static final String[] RATING_REGION_TABLE_US_TV_SUBRATING = {
+ "US_TV_D", "US_TV_L", "US_TV_S", "US_TV_V", "US_TV_FV"
+ };
+
+ // According to ANSI-CEA-766-D
+ private static final int VALUE_US_TV_Y = 1;
+ private static final int VALUE_US_TV_Y7 = 2;
+ private static final int VALUE_US_TV_NONE = 1;
+ private static final int VALUE_US_TV_G = 2;
+ private static final int VALUE_US_TV_PG = 3;
+ private static final int VALUE_US_TV_14 = 4;
+ private static final int VALUE_US_TV_MA = 5;
+
+ private static final int DIMENSION_US_TV_RATING = 0;
+ private static final int DIMENSION_US_TV_D = 1;
+ private static final int DIMENSION_US_TV_L = 2;
+ private static final int DIMENSION_US_TV_S = 3;
+ private static final int DIMENSION_US_TV_V = 4;
+ private static final int DIMENSION_US_TV_Y = 5;
+ private static final int DIMENSION_US_TV_FV = 6;
+ private static final int DIMENSION_US_MV_RATING = 7;
+
+ private static final int VALUE_US_MV_G = 2;
+ private static final int VALUE_US_MV_PG = 3;
+ private static final int VALUE_US_MV_PG13 = 4;
+ private static final int VALUE_US_MV_R = 5;
+ private static final int VALUE_US_MV_NC17 = 6;
+ private static final int VALUE_US_MV_X = 7;
+
+ private static final String STRING_US_TV_Y = "US_TV_Y";
+ private static final String STRING_US_TV_Y7 = "US_TV_Y7";
+ private static final String STRING_US_TV_FV = "US_TV_FV";
+
+
/*
* The following CRC table is from the code generated by the following command.
* $ python pycrc.py --model crc-32-mpeg --algorithm table-driven --generate c
@@ -330,6 +392,7 @@ public class SectionParser {
void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber);
void onEitParsed(int sourceId, List<EitItem> items);
void onEttParsed(int sourceId, List<EttItem> descriptions);
+ void onSdtParsed(List<SdtItem> items);
}
private final OutputListener mListener;
@@ -367,6 +430,10 @@ public class SectionParser {
mParsedEttItems.clear();
}
+ public void resetVersionNumbers() {
+ mSectionVersionMap.clear();
+ }
+
private void parseSection(byte[] data) {
if (!checkSanity(data)) {
Log.d(TAG, "Bad CRC!");
@@ -410,6 +477,13 @@ public class SectionParser {
case TABLE_ID_ETT:
result = parseETT(data);
break;
+ case TABLE_ID_SDT:
+ result = parseSDT(data);
+ break;
+ case TABLE_ID_DVB_ACTUAL_P_F_EIT:
+ case TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT:
+ result = parseDVBEIT(data);
+ break;
default:
break;
}
@@ -510,10 +584,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);
}
@@ -704,6 +776,127 @@ public class SectionParser {
return true;
}
+ private boolean parseSDT(byte[] data) {
+ // For details of the structure for SDT, see DVB Document A038 Table 5.
+ if (DEBUG) {
+ Log.d(TAG, "SDT id discovered");
+ }
+ if (data.length <= 11) {
+ Log.e(TAG, "Broken SDT.");
+ return false;
+ }
+ if ((data[1] & 0x80) >> 7 != 1) {
+ Log.e(TAG, "Broken SDT, section syntax indicator error.");
+ return false;
+ }
+ int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff);
+ int transportStreamId = ((data[3] & 0xff) << 8) | (data[4] & 0xff);
+ int originalNetworkId = ((data[8] & 0xff) << 8) | (data[9] & 0xff);
+ int pos = 11;
+ if (sectionLength + 3 > data.length) {
+ Log.e(TAG, "Broken SDT.");
+ }
+ List<SdtItem> sdtItems = new ArrayList<>();
+ while (pos + 9 < data.length) {
+ int serviceId = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff);
+ int descriptorsLength = ((data[pos + 3] & 0x0f) << 8) | (data[pos + 4] & 0xff);
+ pos += 5;
+ List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + descriptorsLength);
+ List<ServiceDescriptor> serviceDescriptors = generateServiceDescriptors(descriptors);
+ String serviceName = "";
+ String serviceProviderName = "";
+ int serviceType = 0;
+ for (ServiceDescriptor serviceDescriptor : serviceDescriptors) {
+ serviceName = serviceDescriptor.getServiceName();
+ serviceProviderName = serviceDescriptor.getServiceProviderName();
+ serviceType = serviceDescriptor.getServiceType();
+ }
+ if (serviceDescriptors.size() > 0) {
+ sdtItems.add(new SdtItem(serviceName, serviceProviderName, serviceType, serviceId,
+ originalNetworkId));
+ }
+ pos += descriptorsLength;
+ }
+ if (mListener != null) {
+ mListener.onSdtParsed(sdtItems);
+ }
+ return true;
+ }
+
+ private boolean parseDVBEIT(byte[] data) {
+ // For details of the structure for DVB ETT, see DVB Document A038 Table 7.
+ if (DEBUG) {
+ Log.d(TAG, "DVB EIT is discovered.");
+ }
+ if (data.length < 18) {
+ Log.e(TAG, "Broken DVB EIT.");
+ return false;
+ }
+ int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff);
+ int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff);
+ int transportStreamId = ((data[8] & 0xff) << 8) | (data[9] & 0xff);
+ int originalNetworkId = ((data[10] & 0xff) << 8) | (data[11] & 0xff);
+
+ int pos = 14;
+ List<EitItem> results = new ArrayList<>();
+ while (pos + 12 < data.length) {
+ int eventId = ((data[pos] & 0xff) << 8) + (data[pos + 1] & 0xff);
+ float modifiedJulianDate = ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff);
+ int startYear = (int) ((modifiedJulianDate - 15078.2f) / 365.25f);
+ int mjdMonth = (int) ((modifiedJulianDate - 14956.1f
+ - (int) (startYear * 365.25f)) / 30.6001f);
+ int startDay = (int) modifiedJulianDate - 14956 - (int) (startYear * 365.25f)
+ - (int) (mjdMonth * 30.6001f);
+ int startMonth = mjdMonth - 1;
+ if (mjdMonth == 14 || mjdMonth == 15) {
+ startYear += 1;
+ startMonth -= 12;
+ }
+ int startHour = ((data[pos + 4] & 0xf0) >> 4) * 10 + (data[pos + 4] & 0x0f);
+ int startMinute = ((data[pos + 5] & 0xf0) >> 4) * 10 + (data[pos + 5] & 0x0f);
+ int startSecond = ((data[pos + 6] & 0xf0) >> 4) * 10 + (data[pos + 6] & 0x0f);
+ Calendar calendar = Calendar.getInstance();
+ startYear += 1900;
+ calendar.set(startYear, startMonth, startDay, startHour, startMinute, startSecond);
+ long startTime = ConvertUtils.convertUnixEpochToGPSTime(
+ calendar.getTimeInMillis() / 1000);
+ int durationInSecond = (((data[pos + 7] & 0xf0) >> 4) * 10
+ + (data[pos + 7] & 0x0f)) * 3600
+ + (((data[pos + 8] & 0xf0) >> 4) * 10 + (data[pos + 8] & 0x0f)) * 60
+ + (((data[pos + 9] & 0xf0) >> 4) * 10 + (data[pos + 9] & 0x0f));
+ int descriptorsLength = ((data[pos + 10] & 0x0f) << 8)
+ | (data[pos + 10 + 1] & 0xff);
+ int descriptorsPos = pos + 10 + 2;
+ if (data.length < descriptorsPos + descriptorsLength) {
+ Log.e(TAG, "Broken EIT.");
+ return false;
+ }
+ List<TsDescriptor> descriptors = parseDescriptors(
+ data, descriptorsPos, descriptorsPos + descriptorsLength);
+ if (DEBUG) {
+ Log.d(TAG, String.format("DVB EIT descriptors size: %d", descriptors.size()));
+ }
+ // TODO: Add logic to generating content rating for dvb. See DVB document 6.2.28 for
+ // details. Content rating here will be null
+ String contentRating = generateContentRating(descriptors);
+ // TODO: Add logic for generating genre for dvb. See DVB document 6.2.9 for details.
+ // Genre here will be null here.
+ String broadcastGenre = generateBroadcastGenre(descriptors);
+ String canonicalGenre = generateCanonicalGenre(descriptors);
+ String titleText = generateShortEventName(descriptors);
+ List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors);
+ List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors);
+ pos += 12 + descriptorsLength;
+ results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText,
+ startTime, durationInSecond, contentRating, audioTracks, captionTracks,
+ broadcastGenre, canonicalGenre, null));
+ }
+ if (mListener != null) {
+ mListener.onEitParsed(sourceId, results);
+ }
+ return true;
+ }
+
private static List<AtscAudioTrack> generateAudioTracks(List<TsDescriptor> descriptors) {
// The list of audio tracks sent is located at both AC3 Audio descriptor and ISO 639
// Language descriptor.
@@ -717,6 +910,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();
@@ -787,44 +983,179 @@ public class SectionParser {
return services;
}
- private static String generateContentRating(List<TsDescriptor> descriptors) {
- List<String> contentRatings = new ArrayList<>();
+ @VisibleForTesting
+ static String generateContentRating(List<TsDescriptor> descriptors) {
+ Set<String> contentRatings = new ArraySet<>();
+ List<RatingRegion> usRatingRegions = getRatingRegions(descriptors, RATING_REGION_US_TV);
+ List<RatingRegion> krRatingRegions = getRatingRegions(descriptors, RATING_REGION_KR_TV);
+ for (RatingRegion region : usRatingRegions) {
+ String contentRating = getUsRating(region);
+ if (contentRating != null) {
+ contentRatings.add(contentRating);
+ }
+ }
+ for (RatingRegion region : krRatingRegions) {
+ String contentRating = getKrRating(region);
+ if (contentRating != null) {
+ contentRatings.add(contentRating);
+ }
+ }
+ return TextUtils.join(",", contentRatings);
+ }
+
+ /**
+ * Gets a list of {@link RatingRegion} in the specific region.
+ *
+ * @param descriptors {@link TsDescriptor} list which may contains rating information
+ * @param region the specific region
+ * @return a list of {@link RatingRegion} in the specific region
+ */
+ private static List<RatingRegion> getRatingRegions(List<TsDescriptor> descriptors, int region) {
+ List<RatingRegion> ratingRegions = new ArrayList<>();
for (TsDescriptor descriptor : descriptors) {
- if (descriptor instanceof ContentAdvisoryDescriptor) {
- ContentAdvisoryDescriptor contentAdvisoryDescriptor =
- (ContentAdvisoryDescriptor) descriptor;
- for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) {
- for (RegionalRating index : ratingRegion.getRegionalRatings()) {
- String ratingSystem = null;
- String rating = null;
- switch (ratingRegion.getName()) {
- case RATING_REGION_US_TV:
- ratingSystem = RATING_REGION_RATING_SYSTEM_US_TV;
- if (index.getDimension() == 0 && index.getRating() >= 0
- && index.getRating() < RATING_REGION_TABLE_US_TV.length) {
- rating = RATING_REGION_TABLE_US_TV[index.getRating()];
- }
- break;
- case RATING_REGION_KR_TV:
- ratingSystem = RATING_REGION_RATING_SYSTEM_KR_TV;
- if (index.getDimension() == 0 && index.getRating() >= 0
- && index.getRating() < RATING_REGION_TABLE_KR_TV.length) {
- rating = RATING_REGION_TABLE_KR_TV[index.getRating()];
- }
- break;
- default:
- break;
+ if (!(descriptor instanceof ContentAdvisoryDescriptor)) {
+ continue;
+ }
+ ContentAdvisoryDescriptor contentAdvisoryDescriptor =
+ (ContentAdvisoryDescriptor) descriptor;
+ for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) {
+ if (ratingRegion.getName() == region) {
+ ratingRegions.add(ratingRegion);
+ }
+ }
+ }
+ return ratingRegions;
+ }
+
+ /**
+ * Gets US content rating and subratings (if any).
+ *
+ * @param ratingRegion a {@link RatingRegion} instance which may contain rating information.
+ * @return A string representing the US content rating and subratings. The format of the string
+ * is defined in {@link TvContentRating}. null, if no such a string exists.
+ */
+ private static String getUsRating(RatingRegion ratingRegion) {
+ if (ratingRegion.getName() != RATING_REGION_US_TV) {
+ return null;
+ }
+ List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings();
+ String rating = null;
+ int ratingIndex = VALUE_US_TV_NONE;
+ List<String> subratings = new ArrayList<>();
+ for (RegionalRating index : regionalRatings) {
+ // See Table 3 of ANSI-CEA-766-D
+ int dimension = index.getDimension();
+ int value = index.getRating();
+ switch (dimension) {
+ // According to Table 6.27 of ATSC A65,
+ // the dimensions shall be in increasing order.
+ // Therefore, rating and ratingIndex are assigned before any corresponding
+ // subrating.
+ case DIMENSION_US_TV_RATING:
+ if (value >= VALUE_US_TV_G && value < RATING_REGION_TABLE_US_TV.length) {
+ rating = RATING_REGION_TABLE_US_TV[value];
+ ratingIndex = value;
+ }
+ break;
+ case DIMENSION_US_TV_D:
+ if (value == 1
+ && (ratingIndex == VALUE_US_TV_PG || ratingIndex == VALUE_US_TV_14)) {
+ // US_TV_D is applicable to US_TV_PG and US_TV_14
+ subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]);
+ }
+ break;
+ case DIMENSION_US_TV_L:
+ case DIMENSION_US_TV_S:
+ case DIMENSION_US_TV_V:
+ if (value == 1
+ && ratingIndex >= VALUE_US_TV_PG
+ && ratingIndex <= VALUE_US_TV_MA) {
+ // US_TV_L, US_TV_S, and US_TV_V are applicable to
+ // US_TV_PG, US_TV_14 and US_TV_MA
+ subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]);
+ }
+ break;
+ case DIMENSION_US_TV_Y:
+ if (rating == null) {
+ if (value == VALUE_US_TV_Y) {
+ rating = STRING_US_TV_Y;
+ } else if (value == VALUE_US_TV_Y7) {
+ rating = STRING_US_TV_Y7;
}
- if (ratingSystem != null && rating != null) {
- contentRatings.add(TvContentRating
- .createRating("com.android.tv", ratingSystem, rating)
- .flattenToString());
+ }
+ break;
+ case DIMENSION_US_TV_FV:
+ if (STRING_US_TV_Y7.equals(rating) && value == 1) {
+ // US_TV_FV is applicable to US_TV_Y7
+ subratings.add(STRING_US_TV_FV);
+ }
+ break;
+ case DIMENSION_US_MV_RATING:
+ if (value >= VALUE_US_MV_G && value <= VALUE_US_MV_X) {
+ if (value == VALUE_US_MV_X) {
+ // US_MV_X was replaced by US_MV_NC17 in 1990,
+ // and it's not supported by TvContentRating
+ value = VALUE_US_MV_NC17;
+ }
+ if (rating != null) {
+ // According to Table 3 of ANSI-CEA-766-D,
+ // DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING shall not be
+ // present in the same descriptor.
+ Log.w(
+ TAG,
+ "DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING are "
+ + "present in the same descriptor");
+ } else {
+ return TvContentRating.createRating(
+ RATING_DOMAIN,
+ RATING_REGION_RATING_SYSTEM_US_MV,
+ RATING_REGION_TABLE_US_MV[value - 2])
+ .flattenToString();
}
}
- }
+ break;
+
+ default:
+ break;
}
}
- return TextUtils.join(",", contentRatings);
+ if (rating == null) {
+ return null;
+ }
+
+ String[] subratingArray = subratings.toArray(new String[subratings.size()]);
+ return TvContentRating.createRating(
+ RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_US_TV, rating, subratingArray)
+ .flattenToString();
+ }
+
+ /**
+ * Gets KR(South Korea) content rating.
+ *
+ * @param ratingRegion a {@link RatingRegion} instance which may contain rating information.
+ * @return A string representing the KR content rating. The format of the string is defined in
+ * {@link TvContentRating}. null, if no such a string exists.
+ */
+ private static String getKrRating(RatingRegion ratingRegion) {
+ if (ratingRegion.getName() != RATING_REGION_KR_TV) {
+ return null;
+ }
+ List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings();
+ String rating = null;
+ for (RegionalRating index : regionalRatings) {
+ if (index.getDimension() == 0
+ && index.getRating() >= 0
+ && index.getRating() < RATING_REGION_TABLE_KR_TV.length) {
+ rating = RATING_REGION_TABLE_KR_TV[index.getRating()];
+ break;
+ }
+ }
+ if (rating == null) {
+ return null;
+ }
+ return TvContentRating.createRating(
+ RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_KR_TV, rating)
+ .flattenToString();
}
private static String generateBroadcastGenre(List<TsDescriptor> descriptors) {
@@ -849,6 +1180,28 @@ public class SectionParser {
return null;
}
+ private static List<ServiceDescriptor> generateServiceDescriptors(
+ List<TsDescriptor> descriptors) {
+ List<ServiceDescriptor> serviceDescriptors = new ArrayList<>();
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof ServiceDescriptor) {
+ ServiceDescriptor serviceDescriptor = (ServiceDescriptor) descriptor;
+ serviceDescriptors.add(serviceDescriptor);
+ }
+ }
+ return serviceDescriptors;
+ }
+
+ private static String generateShortEventName(List<TsDescriptor> descriptors) {
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof ShortEventDescriptor) {
+ ShortEventDescriptor shortEventDescriptor = (ShortEventDescriptor) descriptor;
+ return shortEventDescriptor.getEventName();
+ }
+ }
+ return "";
+ }
+
private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) {
// For details of the structure for descriptors, see ATSC A/65 Section 6.9.
List<TsDescriptor> descriptors = new ArrayList<>();
@@ -894,6 +1247,22 @@ public class SectionParser {
descriptor = parseIso639Language(data, pos, pos + length + 2);
break;
+ case DVB_DESCRIPTOR_TAG_SERVICE:
+ descriptor = parseDvbService(data, pos, pos + length + 2);
+ break;
+
+ case DVB_DESCRIPTOR_TAG_SHORT_EVENT:
+ descriptor = parseDvbShortEvent(data, pos, pos + length + 2);
+ break;
+
+ case DVB_DESCRIPTOR_TAG_CONTENT:
+ descriptor = parseDvbContent(data, pos, pos + length + 2);
+ break;
+
+ case DVB_DESCRIPTOR_TAG_PARENTAL_RATING:
+ descriptor = parseDvbParentalRating(data, pos, pos + length + 2);
+ break;
+
default:
}
if (descriptor != null) {
@@ -948,6 +1317,7 @@ public class SectionParser {
pos += 3;
boolean ccType = (data[pos] & 0x80) != 0;
if (!ccType) {
+ pos +=3;
continue;
}
int captionServiceNumber = data[pos] & 0x3f;
@@ -987,6 +1357,7 @@ public class SectionParser {
int ratingRegion = data[pos] & 0xff;
int dimensionCount = data[pos + 1] & 0xff;
pos += 2;
+ int previousDimension = -1;
for (int j = 0; j < dimensionCount; ++j) {
if (limit <= pos + 1) {
Log.e(TAG, "Broken ContentAdvisory");
@@ -994,6 +1365,13 @@ public class SectionParser {
}
int dimensionIndex = data[pos] & 0xff;
int ratingValue = data[pos + 1] & 0x0f;
+ if (dimensionIndex <= previousDimension) {
+ // According to Table 6.27 of ATSC A65,
+ // the indices shall be in increasing order.
+ Log.e(TAG, "Broken ContentAdvisory");
+ return null;
+ }
+ previousDimension = dimensionIndex;
pos += 2;
indices.add(new RegionalRating(dimensionIndex, ratingValue));
}
@@ -1189,6 +1567,74 @@ public class SectionParser {
language, language2);
}
+ private static TsDescriptor parseDvbService(byte[] data, int pos, int limit) {
+ // For details of DVB service descriptors, see DVB Document A038 Table 86.
+ if (limit < pos + 5) {
+ Log.e(TAG, "Broken service descriptor.");
+ return null;
+ }
+ pos += 2;
+ int serviceType = data[pos] & 0xff;
+ pos++;
+ int serviceProviderNameLength = data[pos] & 0xff;
+ pos++;
+ String serviceProviderName = extractTextFromDvb(data, pos, serviceProviderNameLength);
+ pos += serviceProviderNameLength;
+ int serviceNameLength = data[pos] & 0xff;
+ pos++;
+ String serviceName = extractTextFromDvb(data, pos, serviceNameLength);
+ return new ServiceDescriptor(serviceType, serviceProviderName, serviceName);
+ }
+
+ private static TsDescriptor parseDvbShortEvent(byte[] data, int pos, int limit) {
+ // For details of DVB service descriptors, see DVB Document A038 Table 91.
+ if (limit < pos + 7) {
+ Log.e(TAG, "Broken short event descriptor.");
+ return null;
+ }
+ pos += 2;
+ String language = new String(data, pos, 3);
+ int eventNameLength = data[pos + 3] & 0xff;
+ pos += 4;
+ if (pos + eventNameLength > limit) {
+ Log.e(TAG, "Broken short event descriptor.");
+ return null;
+ }
+ String eventName = new String(data, pos, eventNameLength);
+ pos += eventNameLength;
+ int textLength = data[pos] & 0xff;
+ if (pos + textLength > limit) {
+ Log.e(TAG, "Broken short event descriptor.");
+ return null;
+ }
+ pos++;
+ String text = new String(data, pos, textLength);
+ return new ShortEventDescriptor(language, eventName, text);
+ }
+
+ private static TsDescriptor parseDvbContent(byte[] data, int pos, int limit) {
+ // TODO: According to DVB Document A038 Table 27 to add a parser for content descriptor to
+ // get content genre.
+ return null;
+ }
+
+ private static TsDescriptor parseDvbParentalRating(byte[] data, int pos, int limit) {
+ // For details of DVB service descriptors, see DVB Document A038 Table 81.
+ HashMap<String, Integer> ratings = new HashMap<>();
+ pos += 2;
+ while (pos + 4 <= limit) {
+ String countryCode = new String(data, pos, 3);
+ int rating = data[pos + 3] & 0xff;
+ pos += 4;
+ if (rating > 15) {
+ // Rating > 15 means that the ratings is defined by broadcaster.
+ continue;
+ }
+ ratings.put(countryCode, rating + 3);
+ }
+ return new ParentalRatingDescriptor(ratings);
+ }
+
private static int getShortNameSize(byte[] data, int offset) {
for (int i = 0; i < MAX_SHORT_NAME_BYTES; i += 2) {
if (data[offset + i] == 0 && data[offset + i + 1] == 0) {
@@ -1244,6 +1690,55 @@ public class SectionParser {
return null;
}
+ private static String extractTextFromDvb(byte[] data, int pos, int length) {
+ // For details of DVB character set selection, see DVB Document A038 Annex A.
+ if (data.length < pos + length) {
+ return null;
+ }
+ try {
+ String charsetPrefix = "ISO-8859-";
+ switch (data[0]) {
+ case 0x01:
+ case 0x02:
+ case 0x03:
+ case 0x04:
+ case 0x05:
+ case 0x06:
+ case 0x07:
+ case 0x09:
+ case 0x0A:
+ case 0x0B:
+ String charset = charsetPrefix + String.valueOf(data[0] & 0xff + 4);
+ return new String(data, pos, length, charset);
+ case 0x10:
+ if (length < 3) {
+ Log.e(TAG, "Broken DVB text");
+ return null;
+ }
+ int codeTable = data[pos + 2] & 0xff;
+ if (data[pos + 1] == 0 && codeTable > 0 && codeTable < 15) {
+ return new String(
+ data, pos, length, charsetPrefix + String.valueOf(codeTable));
+ } else {
+ return new String(data, pos, length, "ISO-8859-1");
+ }
+ case 0x11:
+ case 0x14:
+ case 0x15:
+ return new String(data, pos, length, "UTF-16BE");
+ case 0x12:
+ return new String(data, pos, length, "EUC-KR");
+ case 0x13:
+ return new String(data, pos, length, "GB2312");
+ default:
+ return new String(data, pos, length, "ISO-8859-1");
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.e(TAG, "Unsupported text format.", e);
+ }
+ return new String(data, pos, length);
+ }
+
private static boolean checkSanity(byte[] data) {
if (data.length <= 1) {
return false;
diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java
index c24c2a21..7cdb534e 100644
--- a/src/com/android/tv/tuner/ts/TsParser.java
+++ b/src/com/android/tv/tuner/ts/TsParser.java
@@ -25,6 +25,7 @@ import com.android.tv.tuner.data.PsiData.PmtItem;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.EttItem;
import com.android.tv.tuner.data.PsipData.MgtItem;
+import com.android.tv.tuner.data.PsipData.SdtItem;
import com.android.tv.tuner.data.PsipData.VctItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.ts.SectionParser.OutputListener;
@@ -46,6 +47,8 @@ public class TsParser {
public static final int ATSC_SI_BASE_PID = 0x1ffb;
public static final int PAT_PID = 0x0000;
+ public static final int DVB_SDT_PID = 0x0011;
+ public static final int DVB_EIT_PID = 0x0012;
private static final int TS_PACKET_START_CODE = 0x47;
private static final int TS_PACKET_TEI_MASK = 0x80;
private static final int TS_PACKET_SIZE = 188;
@@ -64,6 +67,7 @@ public class TsParser {
private final Map<Integer, VctItem> mProgramNumberToVctItemMap = new HashMap<>();
private final Map<Integer, List<PmtItem>> mProgramNumberToPMTMap = new HashMap<>();
private final Map<Integer, List<EitItem>> mSourceIdToEitMap = new HashMap<>();
+ private final Map<Integer, SdtItem> mProgramNumberToSdtItemMap = new HashMap<>();
private final Map<EventSourceEntry, List<EitItem>> mEitMap = new HashMap<>();
private final Map<EventSourceEntry, List<EttItem>> mETTMap = new HashMap<>();
private final TreeSet<Integer> mEITPids = new TreeSet<>();
@@ -71,6 +75,7 @@ public class TsParser {
private final SparseBooleanArray mProgramNumberHandledStatus = new SparseBooleanArray();
private final SparseBooleanArray mVctItemHandledStatus = new SparseBooleanArray();
private final TsOutputListener mListener;
+ private final boolean mIsDvbSignal;
private int mVctItemCount;
private int mHandledVctItemCount;
@@ -84,6 +89,7 @@ public class TsParser {
void onEitItemParsed(VctItem channel, List<EitItem> items);
void onEttPidDetected(int pid);
void onAllVctItemsParsed();
+ void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems);
}
private abstract class Stream {
@@ -102,6 +108,7 @@ public class TsParser {
}
protected abstract void handleData(byte[] data, boolean startIndicator);
+ protected abstract void resetDataVersions();
}
private class SectionStream extends Stream {
@@ -138,6 +145,11 @@ public class TsParser {
mSectionParser.parseSections(mPacket);
}
+ @Override
+ protected void resetDataVersions() {
+ mSectionParser.resetVersionNumbers();
+ }
+
private final OutputListener mSectionListener = new OutputListener() {
@Override
public void onPatParsed(List<PatItem> items) {
@@ -173,6 +185,12 @@ public class TsParser {
mListener.onAllVctItemsParsed();
}
}
+ SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber);
+ if (sdtItem != null) {
+ // When PMT is parsed later than SDT.
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleSdtItem(sdtItem, items);
+ }
}
}
@@ -276,6 +294,24 @@ public class TsParser {
mETTMap.put(entry, descriptions);
handleEvents(sourceId);
}
+
+ @Override
+ public void onSdtParsed(List<SdtItem> sdtItems) {
+ for (SdtItem sdtItem : sdtItems) {
+ if (DEBUG) Log.d(TAG, "onSdtParsed " + sdtItem);
+ int programNumber = sdtItem.getServiceId();
+ mProgramNumberToSdtItemMap.put(programNumber, sdtItem);
+ List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber);
+ if (pmtList != null) {
+ mProgramNumberHandledStatus.put(programNumber, true);
+ handleSdtItem(sdtItem, pmtList);
+ } else {
+ mProgramNumberHandledStatus.put(programNumber, false);
+ Log.i(TAG, "onSdtParsed, but PMT for programNo " + programNumber
+ + " is not found yet.");
+ }
+ }
+ }
};
}
@@ -335,6 +371,15 @@ public class TsParser {
}
}
+ private void handleSdtItem(SdtItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "handleSdtItem " + channel);
+ }
+ if (mListener != null) {
+ mListener.onSdtItemParsed(channel, pmtItems);
+ }
+ }
+
private void handleEvents(int sourceId) {
Map<Integer, EitItem> itemSet = new HashMap<>();
for (int pid : mEITPids) {
@@ -367,17 +412,26 @@ public class TsParser {
handleEitItems(channel, items);
} else {
mVctItemHandledStatus.put(sourceId, false);
- Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet.");
+ if (!mIsDvbSignal) {
+ // Log only when zapping to non-DVB channels, since there is not VCT in DVB signal.
+ Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet.");
+ }
}
}
/**
* Creates MPEG-2 TS parser.
+ *
* @param listener TsOutputListener
*/
- public TsParser(TsOutputListener listener) {
- startListening(ATSC_SI_BASE_PID);
+ public TsParser(TsOutputListener listener, boolean isDvbSignal) {
startListening(PAT_PID);
+ startListening(ATSC_SI_BASE_PID);
+ mIsDvbSignal = isDvbSignal;
+ if (isDvbSignal) {
+ startListening(DVB_EIT_PID);
+ startListening(DVB_SDT_PID);
+ }
mListener = listener;
}
@@ -412,7 +466,7 @@ public class TsParser {
// We are not interested in this packet.
return false;
}
- if (payloadPos > pos + TS_PACKET_SIZE) {
+ if (payloadPos >= pos + TS_PACKET_SIZE) {
if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet.");
return false;
}
@@ -451,4 +505,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..d2b4998a 100644
--- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -25,6 +25,7 @@ import android.content.OperationApplicationException;
import android.database.Cursor;
import android.media.tv.TvContract;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
@@ -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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ 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..dc99118a 100644
--- a/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/EventDetector.java
@@ -21,12 +21,12 @@ 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.TunerChannel;
import com.android.tv.tuner.ts.TsParser;
import com.android.tv.tuner.data.PsiData;
import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
import java.util.ArrayList;
import java.util.HashSet;
@@ -48,10 +48,11 @@ public class EventDetector {
// To prevent channel duplication
private final Set<Integer> mVctProgramNumberSet = new HashSet<>();
+ private final Set<Integer> mSdtProgramNumberSet = new HashSet<>();
private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>();
private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray();
private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray();
- private final EventListener mEventListener;
+ private final List<EventListener> mEventListeners = new ArrayList<>();
private int mFrequency;
private String mModulation;
private int mProgramNumber = ALL_PROGRAM_NUMBERS;
@@ -105,8 +106,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 +120,10 @@ public class EventDetector {
@Override
public void onAllVctItemsParsed() {
- if (mEventListener != null) {
- mEventListener.onChannelScanDone();
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelScanDone();
+ }
}
}
@@ -161,8 +166,47 @@ 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);
+ }
+ }
+ }
+
+ @Override
+ public void onSdtItemParsed(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "onSdtItemParsed SDT " + channel);
+ Log.d(TAG, " PMT " + pmtItems);
+ }
+
+ // Merges the audio and caption tracks located in PMT items into the tracks of the given
+ // tuner channel.
+ TunerChannel tunerChannel = new TunerChannel(channel, pmtItems);
+ List<AtscAudioTrack> audioTracks = new ArrayList<>();
+ List<AtscCaptionTrack> captionTracks = new ArrayList<>();
+ for (PsiData.PmtItem pmtItem : pmtItems) {
+ if (pmtItem.getAudioTracks() != null) {
+ audioTracks.addAll(pmtItem.getAudioTracks());
+ }
+ if (pmtItem.getCaptionTracks() != null) {
+ captionTracks.addAll(pmtItem.getCaptionTracks());
+ }
+ }
+ int channelProgramNumber = channel.getServiceId();
+ tunerChannel.setAudioTracks(audioTracks);
+ tunerChannel.setCaptionTracks(captionTracks);
+ tunerChannel.setFrequency(mFrequency);
+ tunerChannel.setModulation(mModulation);
+ mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
+ boolean found = mSdtProgramNumberSet.contains(channelProgramNumber);
+ if (!found) {
+ mSdtProgramNumberSet.add(channelProgramNumber);
+ }
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelDetected(tunerChannel, !found);
+ }
}
}
};
@@ -196,18 +240,23 @@ 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() {
- mTsParser = new TsParser(mTsOutputListener); // TODO: Use TsParser.reset()
+ // TODO: Use TsParser.reset()
+ int deliverySystemType = mTunerHal.getDeliverySystemType();
+ mTsParser =
+ new TsParser(
+ mTsOutputListener,
+ TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType()));
mPidSet.clear();
mVctProgramNumberSet.clear();
+ mSdtProgramNumberSet.clear();
mVctCaptionTracksFound.clear();
mEitCaptionTracksFound.clear();
mChannelMap.clear();
@@ -258,4 +307,28 @@ public class EventDetector {
public List<TunerChannel> getMalFormedChannels() {
return mTsParser.getMalFormedChannels();
}
+
+ /**
+ * Registers an EventListener.
+ * @param eventListener the listener to be registered
+ */
+ public void registerListener(EventListener eventListener) {
+ if (mTsParser != null) {
+ // Resets the version numbers so that the new listener can receive the EIT items.
+ // Otherwise, each EIT session is handled only once unless there is a new version.
+ mTsParser.resetDataVersions();
+ }
+ mEventListeners.add(eventListener);
+ }
+
+ /**
+ * Unregisters an EventListener.
+ * @param eventListener the listener to be unregistered
+ */
+ public void unregisterListener(EventListener eventListener) {
+ boolean removed = mEventListeners.remove(eventListener);
+ if (!removed && DEBUG) {
+ Log.d(TAG, "Cannot unregister a non-registered listener!");
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
index 61de24f4..99222bf8 100644
--- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
@@ -23,10 +23,11 @@ import android.util.SparseBooleanArray;
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.SdtItem;
import com.android.tv.tuner.data.PsipData.VctItem;
+import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
-import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.source.FileTsStreamer;
import com.android.tv.tuner.ts.TsParser;
import com.android.tv.tuner.tvinput.EventDetector.EventListener;
@@ -49,15 +50,18 @@ public class FileSourceEventDetector {
private TsParser mTsParser;
private final Set<Integer> mVctProgramNumberSet = new HashSet<>();
+ private final Set<Integer> mSdtProgramNumberSet = new HashSet<>();
private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>();
private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray();
private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray();
private final EventListener mEventListener;
+ private final boolean mEnableDvbSignal;
private FileTsStreamer.StreamProvider mStreamProvider;
private int mProgramNumber = ALL_PROGRAM_NUMBERS;
- public FileSourceEventDetector(EventDetector.EventListener listener) {
+ public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) {
mEventListener = listener;
+ mEnableDvbSignal = enableDvbSignal;
}
/**
@@ -74,9 +78,10 @@ public class FileSourceEventDetector {
}
private void reset() {
- mTsParser = new TsParser(mTsOutputListener); // TODO: Use TsParser.reset()
+ mTsParser = new TsParser(mTsOutputListener, mEnableDvbSignal); // TODO: Use TsParser.reset()
mStreamProvider.clearPidFilter();
mVctProgramNumberSet.clear();
+ mSdtProgramNumberSet.clear();
mVctCaptionTracksFound.clear();
mEitCaptionTracksFound.clear();
mChannelMap.clear();
@@ -206,5 +211,39 @@ public class FileSourceEventDetector {
mEventListener.onChannelDetected(tunerChannel, !found);
}
}
+
+ @Override
+ public void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems) {
+ if (DEBUG) {
+ Log.d(TAG, "onSdtItemParsed SDT " + channel);
+ Log.d(TAG, " PMT " + pmtItems);
+ }
+
+ // Merges the audio and caption tracks located in PMT items into the tracks of the given
+ // tuner channel.
+ TunerChannel tunerChannel = TunerChannel.forDvbFile(channel, pmtItems);
+ List<AtscAudioTrack> audioTracks = new ArrayList<>();
+ List<AtscCaptionTrack> captionTracks = new ArrayList<>();
+ for (PmtItem pmtItem : pmtItems) {
+ if (pmtItem.getAudioTracks() != null) {
+ audioTracks.addAll(pmtItem.getAudioTracks());
+ }
+ if (pmtItem.getCaptionTracks() != null) {
+ captionTracks.addAll(pmtItem.getCaptionTracks());
+ }
+ }
+ int channelProgramNumber = channel.getServiceId();
+ tunerChannel.setFilepath(mStreamProvider.getFilepath());
+ tunerChannel.setAudioTracks(audioTracks);
+ tunerChannel.setCaptionTracks(captionTracks);
+ mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel);
+ boolean found = mSdtProgramNumberSet.contains(channelProgramNumber);
+ if (!found) {
+ mSdtProgramNumberSet.add(channelProgramNumber);
+ }
+ if (mEventListener != null) {
+ mEventListener.onChannelDetected(tunerChannel, !found);
+ }
+ }
};
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerDebug.java b/src/com/android/tv/tuner/tvinput/TunerDebug.java
index a7a41ea7..2ddc946a 100644
--- a/src/com/android/tv/tuner/tvinput/TunerDebug.java
+++ b/src/com/android/tv/tuner/tvinput/TunerDebug.java
@@ -55,10 +55,10 @@ public class TunerDebug {
return LazyHolder.INSTANCE;
}
- public static void notifyVideoFrameDrop(long delta) {
+ public static void notifyVideoFrameDrop(int count, long delta) {
// TODO: provide timestamp mismatch information using delta
TunerDebug sTunerDebug = getInstance();
- sTunerDebug.mVideoFrameDrop++;
+ sTunerDebug.mVideoFrameDrop += count;
}
public static int getVideoFrameDrop() {
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index 6ec55e4f..34013bf1 100644
--- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -33,14 +33,18 @@ 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.TunerChannel;
+import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
@@ -53,10 +57,10 @@ import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
@@ -71,6 +75,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS
+ ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", "
+ TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
+ private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PREPARE_RECORDER_POLL_MS = 50;
@@ -80,20 +85,23 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private static final int MSG_STOP_RECORDING = 4;
private static final int MSG_MONITOR_STORAGE_STATUS = 5;
private static final int MSG_RELEASE = 6;
+ private static final int MSG_UPDATE_CC_INFO = 7;
private final RecordingCapability mCapabilities;
public RecordingCapability getCapabilities() {
return mCapabilities;
}
- @IntDef({STATE_IDLE, STATE_TUNED, STATE_RECORDING})
+ @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface DvrSessionState {}
private static final int STATE_IDLE = 1;
- private static final int STATE_TUNED = 2;
- private static final int STATE_RECORDING = 3;
+ private static final int STATE_TUNING = 2;
+ private static final int STATE_TUNED = 3;
+ private static final int STATE_RECORDING = 4;
private static final long CHANNEL_ID_NONE = -1;
+ private static final int MAX_TUNING_RETRY = 6;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
@@ -108,13 +116,16 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private long mRecordStartTime;
private long mRecordEndTime;
private boolean mRecorderRunning;
- private BufferManager mBufferManager;
private SampleExtractor mRecorder;
private final TunerRecordingSession mSession;
@DvrSessionState private int mSessionState = STATE_IDLE;
private final String mInputId;
private Uri mProgramUri;
+ private PsipData.EitItem mCurrenProgram;
+ private List<AtscCaptionTrack> mCaptionTracks;
+ private DvrStorageManager mDvrStorageManager;
+
public TunerRecordingSessionWorker(Context context, String inputId,
ChannelDataManager dataManager, TunerRecordingSession session) {
mRandom.setSeed(System.nanoTime());
@@ -157,6 +168,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
if (mChannel == null || mChannel.compareTo(channel) != 0) {
return;
}
+ mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
mChannelDataManager.notifyEventDetected(channel, items);
}
@@ -178,7 +190,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
@MainThread
public void tune(Uri channelUri) {
mHandler.removeCallbacksAndMessages(null);
- mHandler.obtainMessage(MSG_TUNE, channelUri).sendToTarget();
+ mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
}
/**
@@ -211,11 +223,22 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
switch (msg.what) {
case MSG_TUNE: {
Uri channelUri = (Uri) msg.obj;
+ int retryCount = msg.arg1;
if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
if (doTune(channelUri)) {
- mSession.onTuned(channelUri);
- } else {
- reset();
+ if (mSessionState == STATE_TUNED) {
+ mSession.onTuned(channelUri);
+ } else {
+ Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
+ if (retryCount < MAX_TUNING_RETRY) {
+ Message tuneMsg =
+ mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri);
+ mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
+ } else {
+ mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
+ reset();
+ }
+ }
}
return true;
}
@@ -281,6 +304,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mHandler.getLooper().quitSafely();
return true;
}
+ case MSG_UPDATE_CC_INFO: {
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ updateCaptionTracks(pair.first, pair.second);
+ return true;
+ }
}
return false;
}
@@ -310,20 +339,17 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mRecorder.release();
mRecorder = null;
}
- if (mBufferManager != null) {
- mBufferManager.close();
- mBufferManager = null;
- }
if (mTunerSource != null) {
mSourceManager.releaseDataSource(mTunerSource);
mTunerSource = null;
}
+ mDvrStorageManager = null;
mSessionState = STATE_IDLE;
mRecorderRunning = false;
}
private boolean doTune(Uri channelUri) {
- if (mSessionState != STATE_IDLE) {
+ if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Tuning was requested from wrong status.");
return false;
@@ -333,6 +359,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
return false;
+ } else if (mChannel.isRecordingProhibited()) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
+ return false;
}
if (!mDvrStorageStatusManager.isStorageSufficient()) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
@@ -341,9 +371,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
if (mTunerSource == null) {
- mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
- Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
- return false;
+ // Retry tuning in this case.
+ mSessionState = STATE_TUNING;
+ return true;
}
mSessionState = STATE_TUNED;
return true;
@@ -365,10 +395,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
// Since tuning might be happened a while ago, shifts the start position of tuned source.
mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
- mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true));
mRecordStartTime = System.currentTimeMillis();
- mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this,
- true);
+ mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
+ mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource,
+ new BufferManager(mDvrStorageManager), this, true);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
mSessionState = STATE_RECORDING;
@@ -392,6 +422,34 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Log.i(TAG, "Recording stopped");
}
+ private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
+ if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0
+ || items == null || items.isEmpty()) {
+ return;
+ }
+ PsipData.EitItem currentProgram = getCurrentProgram(items);
+ if (currentProgram == null || !currentProgram.hasCaptionTrack()
+ || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) {
+ return;
+ }
+ mCurrenProgram = currentProgram;
+ mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
+ if (DEBUG) {
+ Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for "
+ + currentProgram);
+ }
+ }
+
+ private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
+ for (PsipData.EitItem item : items) {
+ if (mRecordStartTime >= item.getStartTimeUtcMillis()
+ && mRecordStartTime < item.getEndTimeUtcMillis()) {
+ return item;
+ }
+ }
+ return null;
+ }
+
private static class Program {
private final long mChannelId;
private final String mTitle;
@@ -566,15 +624,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
return;
}
Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
- Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(),
- Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime,
- mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs));
+ long recordEndTime =
+ (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
+ ? System.currentTimeMillis()
+ : mRecordStartTime + lastExtractedPositionUs / 1000;
+ Uri uri =
+ insertRecordedProgram(
+ getRecordedProgram(),
+ mChannel.getChannelId(),
+ Uri.fromFile(mStorageDir).toString(),
+ 1024 * 1024,
+ mRecordStartTime,
+ recordEndTime);
if (uri == null) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Inserting a recording to DB failed");
return;
}
+ mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
mSession.onRecordFinished(uri);
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java
index 5c61402e..44bae908 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -38,12 +38,12 @@ import android.widget.Toast;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.TunerPreferences.TunerPreferencesChangedListener;
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.exoplayer.buffer.BufferManager;
-import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.GlobalSettingsUtils;
import com.android.tv.tuner.util.StatusTextUtils;
import com.android.tv.tuner.util.SystemPropertiesProxy;
@@ -52,7 +52,8 @@ import com.android.tv.tuner.util.SystemPropertiesProxy;
* Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions
* are implemented in {@link TunerSessionWorker}.
*/
-public class TunerSession extends TvInputService.Session implements Handler.Callback {
+public class TunerSession extends TvInputService.Session implements
+ Handler.Callback, TunerPreferencesChangedListener {
private static final String TAG = "TunerSession";
private static final boolean DEBUG = false;
private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug";
@@ -65,8 +66,9 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
public static final int MSG_UI_START_CAPTION_TRACK = 6;
public static final int MSG_UI_STOP_CAPTION_TRACK = 7;
public static final int MSG_UI_RESET_CAPTION_TRACK = 8;
- public static final int MSG_UI_SET_STATUS_TEXT = 9;
- public static final int MSG_UI_TOAST_RESCAN_NEEDED = 10;
+ public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9;
+ public static final int MSG_UI_SET_STATUS_TEXT = 10;
+ public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11;
private final Context mContext;
private final Handler mUiHandler;
@@ -81,8 +83,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 +98,10 @@ 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);
+ TunerPreferences.setTunerPreferencesChangedListener(this);
}
public boolean isReleased() {
@@ -214,6 +213,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
mReleased = true;
mSessionWorker.release();
mUiHandler.removeCallbacksAndMessages(null);
+ TunerPreferences.setTunerPreferencesChangedListener(null);
}
/**
@@ -272,10 +272,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: {
@@ -298,6 +301,10 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
mCaptionTrackRenderer.reset();
return true;
}
+ case MSG_UI_CLEAR_CAPTION_RENDERER: {
+ mCaptionTrackRenderer.clear();
+ return true;
+ }
case MSG_UI_SET_STATUS_TEXT: {
mStatusView.setText((CharSequence) msg.obj);
return true;
@@ -309,4 +316,9 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
}
return false;
}
+
+ @Override
+ public void onTunerPreferencesChanged() {
+ mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED);
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index 5230298e..e7eb017e 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -27,6 +27,7 @@ import android.media.tv.TvContract;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
+import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
@@ -35,6 +36,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;
@@ -45,7 +47,10 @@ import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.ExoPlayer;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
+import com.android.tv.customization.TvCustomizationManager;
+import com.android.tv.customization.TvCustomizationManager.TRICKPLAY_MODE;
import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.tuner.TunerPreferences.TrickplaySetting;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.TvTracksInterface;
@@ -55,20 +60,23 @@ 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.exoplayer.ffmpeg.FfmpegDecoderClient;
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 +90,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;
@@ -93,6 +104,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8;
public static final int MSG_UNBLOCKED_RATING = 9;
+ public static final int MSG_TUNER_PREFERENCES_CHANGED = 10;
// Private messages
private static final int MSG_TUNE = 1000;
@@ -147,10 +159,20 @@ 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;
+ private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14);
+
+ // 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 final @TRICKPLAY_MODE int mTrickplayModeCustomization;
private volatile Surface mSurface;
private volatile float mVolume = 1.0f;
private volatile boolean mCaptionEnabled;
@@ -159,6 +181,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private volatile Long mRecordingDuration;
private volatile long mRecordStartTimeMs;
private volatile long mBufferStartTimeMs;
+ private volatile boolean mTrickplayDisabledByStorageIssue;
+ private @TrickplaySetting int mTrickplaySetting;
+ private long mTrickplayExpiredMs;
private String mRecordingId;
private final Handler mHandler;
private int mRetryCount;
@@ -177,19 +202,20 @@ 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 final boolean mHasSoftwareAudioDecoder;
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,10 +237,39 @@ 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);
+ mTrickplayModeCustomization = TvCustomizationManager.getTrickplayMode(context);
+ if (mTrickplayModeCustomization ==
+ TvCustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+ boolean useExternalStorage =
+ Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) &&
+ Environment.isExternalStorageRemovable();
+ mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null;
+ } else if (mTrickplayModeCustomization == TvCustomizationManager.TRICKPLAY_MODE_ENABLED) {
+ mTrickplayBufferDir = context.getCacheDir();
+ } else {
+ mTrickplayBufferDir = null;
+ }
+ mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null;
+ mTrickplaySetting = TunerPreferences.getTrickplaySetting(context);
+ if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET
+ && mTrickplayModeCustomization
+ == TvCustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) {
+ // Consider the case of Customization package updates the value of trickplay mode
+ // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install.
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET;
+ TunerPreferences.setTrickplaySetting(context, mTrickplaySetting);
+ TunerPreferences.setTrickplayExpiredMs(context, 0);
+ }
+ mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context);
mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
+ // NOTE: We assume that TunerSessionWorker instance will be at most one.
+ // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time.
+ // connect() will return false, if there is a connected TunerSessionWorker already.
+ mHasSoftwareAudioDecoder = FfmpegDecoderClient.connect(context);
}
// Public methods
@@ -285,24 +340,21 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private Long getDurationForRecording(String recordingId) {
- try {
- DvrStorageManager storageManager =
+ DvrStorageManager storageManager =
new DvrStorageManager(new File(getRecordingPath()), false);
- Pair<String, MediaFormat> trackInfo = null;
- try {
- trackInfo = storageManager.readTrackInfoFile(false);
- } catch (FileNotFoundException e) {
- }
- if (trackInfo == null) {
- trackInfo = storageManager.readTrackInfoFile(true);
- }
- Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION);
+ List<BufferManager.TrackFormat> trackFormatList =
+ storageManager.readTrackInfoFiles(false);
+ if (trackFormatList.isEmpty()) {
+ trackFormatList = storageManager.readTrackInfoFiles(true);
+ }
+ if (!trackFormatList.isEmpty()) {
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(0);
+ Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION);
// we need duration by milli for trickplay notification.
return durationUs != null ? durationUs / 1000 : null;
- } catch (IOException e) {
- Log.e(TAG, "meta file for recording was not found: " + recordingId);
- return null;
}
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
}
@MainThread
@@ -341,16 +393,15 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@MainThread
public void release() {
if (DEBUG) Log.d(TAG, "release()");
+ synchronized (mReleaseLock) {
+ mReleaseRequested = true;
+ }
+ if (mHasSoftwareAudioDecoder) {
+ FfmpegDecoderClient.disconnect(mContext);
+ }
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 +418,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 +430,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 +448,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 +467,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;
@@ -461,6 +517,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
@Override
+ public void onClearCaptionEvent() {
+ mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER);
+ }
+
+ @Override
public void onDiscoverCaptionServiceNumber(int serviceNumber) {
sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
}
@@ -499,7 +560,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@Override
public void onDiskTooSlow() {
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ mTrickplayDisabledByStorageIssue = true;
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
}
// EventDetector.EventListener
@@ -602,6 +664,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 +700,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 +709,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 +718,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 +727,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,16 +746,18 @@ 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);
+ Log.i(TAG, "Notify weak signal since fail to retry playback");
// After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically chosen
// value before recovering the playback.
@@ -679,13 +769,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 +881,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 +909,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;
}
@@ -835,6 +927,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mPlayer == null) {
return true;
}
+ setTrickplayEnabledIfNeeded();
doTimeShiftPause();
return true;
}
@@ -843,6 +936,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mPlayer == null) {
return true;
}
+ setTrickplayEnabledIfNeeded();
doTimeShiftResume();
return true;
}
@@ -852,6 +946,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mPlayer == null) {
return true;
}
+ setTrickplayEnabledIfNeeded();
doTimeShiftSeekTo(position);
return true;
}
@@ -859,6 +954,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mPlayer == null) {
return true;
}
+ setTrickplayEnabledIfNeeded();
doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
return true;
}
@@ -883,6 +979,22 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
return true;
}
+ case MSG_TUNER_PREFERENCES_CHANGED: {
+ mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED);
+ @TrickplaySetting int trickplaySetting =
+ TunerPreferences.getTrickplaySetting(mContext);
+ if (trickplaySetting != mTrickplaySetting) {
+ boolean wasTrcikplayEnabled =
+ mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ boolean isTrickplayEnabled =
+ trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ mTrickplaySetting = trickplaySetting;
+ if (isTrickplayEnabled != wasTrcikplayEnabled) {
+ sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer));
+ }
+ }
+ return true;
+ }
case MSG_BUFFER_START_TIME_CHANGED: {
if (mPlayer == null) {
return true;
@@ -891,7 +1003,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (!hasEnoughBackwardBuffer()
&& (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
mPlayer.setPlayWhenReady(true);
- mPlayer.setAudioTrack(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
mPlaybackParams.setSpeed(1.0f);
}
return true;
@@ -909,7 +1021,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,32 +1038,36 @@ 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;
- boolean isPreparingTooLong = mPreparingStartTimeMs != INVALID_TIME
- && currentTime - mPreparingStartTimeMs
- > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ long bufferingTimeMs = mBufferingStartTimeMs != INVALID_TIME
+ ? currentTime - mBufferingStartTimeMs : mBufferingStartTimeMs;
+ long preparingTimeMs = mPreparingStartTimeMs != INVALID_TIME
+ ? currentTime - mPreparingStartTimeMs : mPreparingStartTimeMs;
+ boolean isBufferingTooLong =
+ bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
+ boolean isPreparingTooLong =
+ preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
boolean isWeakSignal = source != null
- && mChannel.getType() == Channel.TYPE_TUNER
- && (noBufferRead || isBufferingTooLong || isPreparingTooLong);
+ && mChannel.getType() != Channel.TYPE_FILE
+ && (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);
+ mPlayer.setAudioTrackAndClosedCaption(false);
}
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
+ Log.i(TAG, "Notify weak signal due to signal check, " + String.format(
+ "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " +
+ "videoFrameDrop:%d",
+ (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE,
+ bufferingTimeMs,
+ preparingTimeMs,
+ TunerDebug.getVideoFrameDrop()
+ ));
} else if (!isWeakSignal && mReportedWeakSignal) {
boolean isPlaybackStable = mReadyStartTimeMs != INVALID_TIME
&& currentTime - mReadyStartTimeMs
@@ -962,11 +1077,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
} else if (mReportedDrawnToSurface) {
mHandler.removeMessages(MSG_RETRY_PLAYBACK);
notifyVideoAvailable();
- mPlayer.setAudioTrack(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
}
}
mLastLimitInBytes = limitInBytes;
- mLastPositionInBytes = positionInBytes;
mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
return true;
}
@@ -999,15 +1113,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 +1137,49 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) {
+ private void setTrickplayEnabledIfNeeded() {
+ if (mChannel == null ||
+ mTrickplayModeCustomization != TvCustomizationManager.TRICKPLAY_MODE_ENABLED) {
+ return;
+ }
+ if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED;
+ TunerPreferences.setTrickplaySetting(
+ mContext, mTrickplaySetting);
+ }
+ }
+
+ private MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
if (capabilities == null) {
Log.w(TAG, "No Audio Capabilities");
}
-
+ long now = System.currentTimeMillis();
+ if (mTrickplayModeCustomization == TvCustomizationManager.TRICKPLAY_MODE_ENABLED
+ && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) {
+ if (mTrickplayExpiredMs == 0) {
+ mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS;
+ TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs);
+ } else {
+ if (mTrickplayExpiredMs < now) {
+ mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting);
+ }
+ }
+ }
+ BufferManager bufferManager = null;
+ if (mRecordingId != null) {
+ StorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ bufferManager = new BufferManager(storageManager);
+ updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles());
+ } else if (!mTrickplayDisabledByStorageIssue
+ && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED
+ && 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 +1214,26 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
- if (DEBUG) {
- Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
- }
- List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
- List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
- // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
- // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
- // track info in PMT more and use info in EIT only when we have nothing.
- if (audioTracks != null && !audioTracks.isEmpty()
- && (mChannel.getAudioTracks() == null || fromPmt)) {
- updateAudioTracks(audioTracks);
- }
- if (captionTracks == null || captionTracks.isEmpty()) {
- if (tvTracksInterface.hasCaptionTrack()) {
+ synchronized (tvTracksInterface) {
+ if (DEBUG) {
+ Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+ }
+ List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+ List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+ // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
+ // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
+ // track info in PMT more and use info in EIT only when we have nothing.
+ if (audioTracks != null && !audioTracks.isEmpty()
+ && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) {
+ updateAudioTracks(audioTracks);
+ }
+ if (captionTracks == null || captionTracks.isEmpty()) {
+ if (tvTracksInterface.hasCaptionTrack()) {
+ updateCaptionTracks(captionTracks);
+ }
+ } else {
updateCaptionTracks(captionTracks);
}
- } else {
- updateCaptionTracks(captionTracks);
}
}
@@ -1132,25 +1279,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 +1372,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();
@@ -1239,14 +1387,15 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
+ mLastLimitInBytes = 0L;
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
}
}
- 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()) {
@@ -1257,9 +1406,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return;
}
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);
+ || (mChannel.hasVideo() && !mPlayer.hasVideo()))
+ && mChannel.getType() != Channel.TYPE_NETWORK) {
+ // If the channel is from network, skip this part since the video and audio tracks
+ // information for channels from network are more reliable in the extractor. Otherwise,
+ // tracks haven't been detected in the extractor. Try again.
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
return;
}
// Since mSurface is volatile, we define a local variable surface to keep the same value
@@ -1269,7 +1421,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mPlayer.setSurface(surface);
mPlayer.setPlayWhenReady(true);
mPlayer.setVolume(mVolume);
- if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) {
+ if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) {
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
} else if (!mReportedWeakSignal) {
// Doesn't show buffering during weak signal.
@@ -1286,22 +1438,21 @@ 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 ?
mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER);
- if (!player.prepare(mContext, mChannel, this)) {
+ if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) {
mSourceManager.setKeepTuneStatus(false);
player.release();
if (!mHandler.hasMessages(MSG_TUNE)) {
// 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);
+ Log.i(TAG, "Notify weak signal due to player preparation failure");
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK,
+ System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS);
}
} else {
mPlayer = player;
@@ -1314,7 +1465,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 +1487,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);
@@ -1385,14 +1540,19 @@ public class TunerSessionWorker implements PlaybackBufferListener,
} else {
mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs);
mPlaybackParams.setSpeed(1.0f);
- mPlayer.setAudioTrack(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
return;
}
} else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
- mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
- mPlaybackParams.setSpeed(1.0f);
- mPlayer.setAudioTrack(true);
- return;
+ // Stops trickplay when FF requested the position later than current position.
+ // If RW trickplay requested the position later than current position,
+ // continue trickplay.
+ if (mPlaybackParams.getSpeed() > 0.0f) {
+ mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
+ mPlaybackParams.setSpeed(1.0f);
+ mPlayer.setAudioTrackAndClosedCaption(true);
+ return;
+ }
}
long delayForNextSeek = getTrickPlaySeekIntervalMs();
@@ -1414,7 +1574,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
mPlaybackParams.setSpeed(1.0f);
mPlayer.setPlayWhenReady(false);
- mPlayer.setAudioTrack(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
}
private void doTimeShiftResume() {
@@ -1422,7 +1582,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
mPlaybackParams.setSpeed(1.0f);
mPlayer.setPlayWhenReady(true);
- mPlayer.setAudioTrack(true);
+ mPlayer.setAudioTrackAndClosedCaption(true);
}
private void doTimeShiftSeekTo(long timeMs) {
@@ -1443,14 +1603,14 @@ public class TunerSessionWorker implements PlaybackBufferListener,
doTimeShiftResume();
} else if (mPlayer.supportSmoothTrickPlay(speed)) {
mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
- mPlayer.setAudioTrack(false);
+ mPlayer.setAudioTrackAndClosedCaption(false);
mPlayer.startSmoothTrickplay(mPlaybackParams);
mHandler.sendEmptyMessageDelayed(MSG_SMOOTH_TRICKPLAY_MONITOR,
TRICKPLAY_MONITOR_INTERVAL_MS);
} else {
mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) {
- mPlayer.setAudioTrack(false);
+ mPlayer.setAudioTrackAndClosedCaption(false);
mPlayer.setPlayWhenReady(false);
// Initiate trickplay
mHandler.sendMessage(mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK,
@@ -1525,8 +1685,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
TvContentRating[] ratings = mTvContentRatingCache
.getRatings(currentProgram.getContentRating());
- if (ratings == null) {
- return null;
+ if (ratings == null || ratings.length == 0) {
+ ratings = new TvContentRating[] {TvContentRating.UNRATED};
}
for (TvContentRating rating : ratings) {
if (!Objects.equals(mUnblockedContentRating, rating) && mTvInputManager
@@ -1544,15 +1704,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 +1722,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/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
index e734b779..6ad00daa 100644
--- a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
+++ b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java
@@ -24,6 +24,7 @@ import android.database.Cursor;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.AsyncTask;
+import android.util.Log;
import com.android.tv.TvApplication;
import com.android.tv.dvr.DvrStorageStatusManager;
@@ -40,10 +41,17 @@ import java.util.concurrent.TimeUnit;
* from database.
*/
public class TunerStorageCleanUpService extends JobService {
+ private static final String TAG = "TunerStorageCleanUpService";
+
private CleanUpStorageTask mTask;
@Override
public void onCreate() {
+ if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ this.stopSelf();
+ return;
+ }
TvApplication.setCurrentRunningProcess(this, false);
super.onCreate();
mTask = new CleanUpStorageTask(this, this);
diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
index 684ebdbd..2725ddfc 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,17 +49,20 @@ public class TunerTvInputService extends TvInputService
private ChannelDataManager mChannelDataManager;
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private AudioCapabilities mAudioCapabilities;
- private BufferManager mBufferManager;
@Override
public void onCreate() {
+ if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) {
+ Log.wtf(TAG, "Stopping because device does not have a TvInputManager");
+ this.stopSelf();
+ return;
+ }
TvApplication.setCurrentRunningProcess(this, false);
super.onCreate();
if (DEBUG) Log.d(TAG, "onCreate");
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 +76,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 +84,6 @@ public class TunerTvInputService extends TvInputService
super.onDestroy();
mChannelDataManager.release();
mAudioCapabilitiesReceiver.unregister();
- if (mBufferManager != null) {
- mBufferManager.close();
- }
}
@Override
@@ -106,8 +95,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 +117,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..9eb689a7
--- /dev/null
+++ b/src/com/android/tv/tuner/util/PostalCodeUtils.java
@@ -0,0 +1,138 @@
+/*
+ * 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.NonNull;
+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.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class to update, get, and set the last known postal or zip code.
+ */
+public class PostalCodeUtils {
+ private static final String TAG = "PostalCodeUtils";
+
+ // Postcode formats, where A signifies a letter and 9 a digit:
+ // US zip code format: 99999
+ private static final String POSTCODE_REGEX_US = "^(\\d{5})";
+ // UK postcode district formats: A9, A99, AA9, AA99
+ // Full UK postcode format: Postcode District + space + 9AA
+ // Should be able to handle both postcode district and full postcode
+ private static final String POSTCODE_REGEX_GB =
+ "^([A-Z][A-Z]?[0-9][0-9A-Z]?)( ?[0-9][A-Z]{2})?$";
+ private static final String POSTCODE_REGEX_GB_GIR = "^GIR( ?0AA)?$"; // special UK postcode
+
+ private static final Map<String, Pattern> REGION_PATTERN = new HashMap<>();
+ private static final Map<String, Integer> REGION_MAX_LENGTH = new HashMap<>();
+
+ static {
+ REGION_PATTERN.put(Locale.US.getCountry(), Pattern.compile(POSTCODE_REGEX_US));
+ REGION_PATTERN.put(
+ Locale.UK.getCountry(),
+ Pattern.compile(POSTCODE_REGEX_GB + "|" + POSTCODE_REGEX_GB_GIR));
+ REGION_MAX_LENGTH.put(Locale.US.getCountry(), 5);
+ REGION_MAX_LENGTH.put(Locale.UK.getCountry(), 8);
+ }
+
+ // The longest postcode number is 10-character-long.
+ // Use a larger number to accommodate future changes.
+ private static final int DEFAULT_MAX_LENGTH = 16;
+
+ /** 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());
+ 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() {
+ }
+ }
+
+ /**
+ * Checks whether a postcode matches the format of the specific region.
+ *
+ * @return {@code false} if the region is supported and the postcode doesn't match; {@code true}
+ * otherwise
+ */
+ public static boolean matches(@NonNull CharSequence postcode, @NonNull String region) {
+ Pattern pattern = REGION_PATTERN.get(region.toUpperCase());
+ return pattern == null || pattern.matcher(postcode).matches();
+ }
+
+ /**
+ * Gets the largest possible postcode length in the region.
+ *
+ * @return maximum postcode length if the region is supported; {@link #DEFAULT_MAX_LENGTH}
+ * otherwise
+ */
+ public static int getRegionMaxLength(Context context) {
+ Integer maxLength =
+ REGION_MAX_LENGTH.get(LocationUtils.getCurrentCountry(context).toUpperCase());
+ return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
index 62a64361..2817ccbf 100644
--- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
+++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
@@ -58,4 +58,20 @@ public class SystemPropertiesProxy {
}
return def;
}
+
+ public static String getString(String key, String def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class);
+ getIntMethod.setAccessible(true);
+ return (String) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.get()", e);
+ }
+ return def;
+ }
}
diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
index 5c411f64..f421bf1a 100644
--- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
+++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
@@ -21,10 +21,11 @@ import android.content.ComponentName;
import android.content.Context;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
+import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.Nullable;
-import android.support.v4.os.BuildCompat;
import android.util.Log;
+import android.util.Pair;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.tuner.R;
@@ -43,23 +44,31 @@ public class TunerInputInfoUtils {
*/
@Nullable
@TargetApi(Build.VERSION_CODES.N)
- public static TvInputInfo buildTunerInputInfo(Context context, boolean fromBuiltInTuner) {
- int numOfDevices = TunerHal.getTunerCount(context);
- if (numOfDevices == 0) {
+ public static TvInputInfo buildTunerInputInfo(Context context) {
+ Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
+ if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
return null;
}
- TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context,
- TunerTvInputService.class));
- if (fromBuiltInTuner) {
- builder.setLabel(R.string.bt_app_name);
- } else {
- builder.setLabel(R.string.ut_app_name);
+ int inputLabelId = 0;
+ switch (tunerTypeAndCount.first) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ inputLabelId = R.string.bt_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ inputLabelId = R.string.ut_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ inputLabelId = R.string.nt_app_name;
+ break;
}
try {
- return builder.setCanRecord(CommonFeatures.DVR.isEnabled(context))
- .setTunerCount(numOfDevices)
+ TvInputInfo.Builder builder = new TvInputInfo.Builder(context,
+ new ComponentName(context, TunerTvInputService.class));
+ return builder.setLabel(inputLabelId)
+ .setCanRecord(CommonFeatures.DVR.isEnabled(context))
+ .setTunerCount(tunerTypeAndCount.second)
.build();
- } catch (NullPointerException e) {
+ } catch (IllegalArgumentException | NullPointerException e) {
// TunerTvInputService is not enabled.
return null;
}
@@ -71,30 +80,36 @@ public class TunerInputInfoUtils {
* @param context {@link Context} instance
*/
public static void updateTunerInputInfo(Context context) {
- if (BuildCompat.isAtLeastN()) {
- if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
- TvInputInfo info = buildTunerInputInfo(context, isBuiltInTuner(context));
- if (info != null) {
- ((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE))
- .updateTvInputInfo(info);
- if (DEBUG) {
- Log.d(TAG, "TvInputInfo [" + info.loadLabel(context)
- + "] updated: " + info.toString());
+ final Context appContext = context.getApplicationContext();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ new AsyncTask<Void, Void, TvInputInfo>() {
+ @Override
+ protected TvInputInfo doInBackground(Void... params) {
+ if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
+ return buildTunerInputInfo(appContext);
}
- } else {
- if (DEBUG) {
- Log.d(TAG, "Updating tuner input's info failed. Input is not ready yet.");
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.N)
+ protected void onPostExecute(TvInputInfo info) {
+ if (info != null) {
+ ((TvInputManager) appContext.getSystemService(Context.TV_INPUT_SERVICE))
+ .updateTvInputInfo(info);
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "TvInputInfo ["
+ + info.loadLabel(appContext)
+ + "] updated: "
+ + info.toString());
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Updating tuner input info failed. Input is not ready yet.");
+ }
+ }
}
- }
+ }.execute();
}
}
-
- /**
- * 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..625014ea 100644
--- a/src/com/android/tv/ui/AppLayerTvView.java
+++ b/src/com/android/tv/ui/AppLayerTvView.java
@@ -22,7 +22,8 @@ import android.util.AttributeSet;
import android.view.SurfaceView;
import android.view.View;
-import com.android.tv.experiments.Experiments;
+import com.android.tv.util.Debug;
+import com.android.tv.util.Utils;
/**
* A TvView class for application layer when multiple windows are being used in the app.
@@ -55,8 +56,17 @@ public class AppLayerTvView extends TvView {
public void onViewAdded(View child) {
if (child instanceof SurfaceView) {
// Note: See b/29118070 for detail.
- ((SurfaceView) child).setSecure(!Experiments.ENABLE_DEVELOPER_FEATURES.get());
+ ((SurfaceView) child).setSecure(!Utils.isDeveloper());
}
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/BlockScreenView.java b/src/com/android/tv/ui/BlockScreenView.java
index 52b9389d..09c167ca 100644
--- a/src/com/android/tv/ui/BlockScreenView.java
+++ b/src/com/android/tv/ui/BlockScreenView.java
@@ -21,32 +21,37 @@ import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
-import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.ui.TunableTvView.BlockScreenType;
-public class BlockScreenView extends LinearLayout {
+public class BlockScreenView extends FrameLayout {
private View mContainerView;
private View mImageContainer;
- private ImageView mNormalImageView;
- private ImageView mShrunkenImageView;
+ private ImageView mNormalLockIconView;
+ private ImageView mShrunkenLockIconView;
private View mSpace;
- private TextView mTextView;
+ private TextView mBlockingInfoTextView;
+ private ImageView mBackgroundImageView;
private final int mSpacingNormal;
private final int mSpacingShrunken;
- // Animators used for fade in/out of block screen icon.
- private Animator mFadeIn;
+ // Animator used to fade out the whole block screen.
private Animator mFadeOut;
+ // Animators used to fade in/out the block screen icon and info text.
+ private Animator mInfoFadeIn;
+ private Animator mInfoFadeOut;
+
public BlockScreenView(Context context) {
this(context, null, 0);
}
@@ -68,21 +73,32 @@ public class BlockScreenView extends LinearLayout {
super.onFinishInflate();
mContainerView = findViewById(R.id.block_screen_container);
mImageContainer = findViewById(R.id.image_container);
- mNormalImageView = (ImageView) findViewById(R.id.block_screen_icon);
- mShrunkenImageView = (ImageView) findViewById(R.id.block_screen_shrunken_icon);
+ mNormalLockIconView = (ImageView) findViewById(R.id.block_screen_icon);
+ mShrunkenLockIconView = (ImageView) findViewById(R.id.block_screen_shrunken_icon);
mSpace = findViewById(R.id.space);
- mTextView = (TextView) findViewById(R.id.block_screen_text);
- mFadeIn = AnimatorInflater.loadAnimator(getContext(),
- R.animator.tvview_block_screen_fade_in);
- mFadeIn.setTarget(mContainerView);
+ mBlockingInfoTextView = (TextView) findViewById(R.id.block_screen_text);
+ mBackgroundImageView = (ImageView) findViewById(R.id.background_image);
mFadeOut = AnimatorInflater.loadAnimator(getContext(),
R.animator.tvview_block_screen_fade_out);
- mFadeOut.setTarget(mContainerView);
+ mFadeOut.setTarget(this);
mFadeOut.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
+ setVisibility(GONE);
+ setBackgroundImage(null);
+ setAlpha(1.0f);
+ }
+ });
+ mInfoFadeIn = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.tvview_block_screen_fade_in);
+ mInfoFadeIn.setTarget(mContainerView);
+ mInfoFadeOut = AnimatorInflater.loadAnimator(getContext(),
+ R.animator.tvview_block_screen_fade_out);
+ mInfoFadeOut.setTarget(mContainerView);
+ mInfoFadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
mContainerView.setVisibility(GONE);
- mContainerView.setAlpha(1f);
}
});
}
@@ -90,53 +106,54 @@ public class BlockScreenView extends LinearLayout {
/**
* Sets the normal image.
*/
- public void setImage(int resId) {
- mNormalImageView.setImageResource(resId);
+ public void setIconImage(int resId) {
+ mNormalLockIconView.setImageResource(resId);
updateSpaceVisibility();
}
/**
* Sets the scale type of the normal image.
*/
- public void setScaleType(ScaleType scaleType) {
- mNormalImageView.setScaleType(scaleType);
+ public void setIconScaleType(ScaleType scaleType) {
+ mNormalLockIconView.setScaleType(scaleType);
updateSpaceVisibility();
}
/**
- * Sets the shrunken image.
+ * Show or hide the image of this view.
*/
- public void setShrunkenImage(int resId) {
- mShrunkenImageView.setImageResource(resId);
+ public void setIconVisibility(boolean visible) {
+ mImageContainer.setVisibility(visible ? VISIBLE : GONE);
updateSpaceVisibility();
}
/**
- * Show or hide the image of this view.
+ * Sets the text message.
*/
- public void setImageVisibility(boolean visible) {
- mImageContainer.setVisibility(visible ? VISIBLE : GONE);
+ public void setInfoText(int resId) {
+ mBlockingInfoTextView.setText(resId);
updateSpaceVisibility();
}
/**
* Sets the text message.
*/
- public void setText(int resId) {
- mTextView.setText(resId);
+ public void setInfoText(String text) {
+ mBlockingInfoTextView.setText(text);
updateSpaceVisibility();
}
/**
- * Sets the text message.
+ * Sets the background image should be displayed in the block screen view. Passes {@code null}
+ * to remove the currently displayed background image.
*/
- public void setText(String text) {
- mTextView.setText(text);
- updateSpaceVisibility();
+ public void setBackgroundImage(Drawable backgroundImage) {
+ mBackgroundImageView.setVisibility(backgroundImage == null ? GONE : VISIBLE);
+ mBackgroundImageView.setImageDrawable(backgroundImage);
}
private void updateSpaceVisibility() {
- if (isImageViewVisible() && isTextViewVisible(mTextView)) {
+ if (isImageViewVisible() && isTextViewVisible(mBlockingInfoTextView)) {
mSpace.setVisibility(VISIBLE);
} else {
mSpace.setVisibility(GONE);
@@ -145,7 +162,8 @@ public class BlockScreenView extends LinearLayout {
private boolean isImageViewVisible() {
return mImageContainer.getVisibility() == VISIBLE
- && (isImageViewVisible(mNormalImageView) || isImageViewVisible(mShrunkenImageView));
+ && (isImageViewVisible(mNormalLockIconView)
+ || isImageViewVisible(mShrunkenLockIconView));
}
private static boolean isImageViewVisible(ImageView imageView) {
@@ -177,37 +195,39 @@ public class BlockScreenView extends LinearLayout {
mContainerView.setVisibility(GONE);
break;
case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- mNormalImageView.setVisibility(GONE);
- mShrunkenImageView.setVisibility(VISIBLE);
+ mNormalLockIconView.setVisibility(GONE);
+ mShrunkenLockIconView.setVisibility(VISIBLE);
mContainerView.setVisibility(VISIBLE);
+ mContainerView.setAlpha(1.0f);
break;
case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL:
- mNormalImageView.setVisibility(VISIBLE);
- mShrunkenImageView.setVisibility(GONE);
+ mNormalLockIconView.setVisibility(VISIBLE);
+ mShrunkenLockIconView.setVisibility(GONE);
mContainerView.setVisibility(VISIBLE);
+ mContainerView.setAlpha(1.0f);
break;
}
} else {
switch (blockScreenType) {
case TunableTvView.BLOCK_SCREEN_TYPE_NO_UI:
if (mContainerView.getVisibility() == VISIBLE) {
- mFadeOut.start();
+ mInfoFadeOut.start();
}
break;
case TunableTvView.BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
- mNormalImageView.setVisibility(GONE);
- mShrunkenImageView.setVisibility(VISIBLE);
- mContainerView.setVisibility(VISIBLE);
+ mNormalLockIconView.setVisibility(GONE);
+ mShrunkenLockIconView.setVisibility(VISIBLE);
if (mContainerView.getVisibility() == GONE) {
- mFadeIn.start();
+ mContainerView.setVisibility(VISIBLE);
+ mInfoFadeIn.start();
}
break;
case TunableTvView.BLOCK_SCREEN_TYPE_NORMAL:
- mNormalImageView.setVisibility(VISIBLE);
- mShrunkenImageView.setVisibility(GONE);
- mContainerView.setVisibility(VISIBLE);
+ mNormalLockIconView.setVisibility(VISIBLE);
+ mShrunkenLockIconView.setVisibility(GONE);
if (mContainerView.getVisibility() == GONE) {
- mFadeIn.start();
+ mContainerView.setVisibility(VISIBLE);
+ mInfoFadeIn.start();
}
break;
}
@@ -216,26 +236,33 @@ public class BlockScreenView extends LinearLayout {
}
/**
- * Scales the contents view by the given {@code scale}.
+ * Adds a listener to the fade-in animation of info text and icons of the block screen.
*/
- public void scaleContainerView(float scale) {
- mContainerView.setScaleX(scale);
- mContainerView.setScaleY(scale);
+ public void addInfoFadeInAnimationListener(AnimatorListener listener) {
+ mInfoFadeIn.addListener(listener);
}
- public void addFadeOutAnimationListener(AnimatorListener listener) {
- mFadeOut.addListener(listener);
+ /**
+ * Fades out the block screen.
+ */
+ public void fadeOut() {
+ if (getVisibility() == VISIBLE && !mFadeOut.isStarted()) {
+ mFadeOut.start();
+ }
}
/**
* Ends the currently running animations.
*/
public void endAnimations() {
- if (mFadeIn != null && mFadeIn.isRunning()) {
- mFadeIn.end();
- }
if (mFadeOut != null && mFadeOut.isRunning()) {
mFadeOut.end();
}
+ if (mInfoFadeIn != null && mInfoFadeIn.isRunning()) {
+ mInfoFadeIn.end();
+ }
+ if (mInfoFadeOut != null && mInfoFadeOut.isRunning()) {
+ mInfoFadeOut.end();
+ }
}
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index 3cf4de83..a5d897f2 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -23,12 +23,9 @@ import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
-import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.media.tv.TvContentRating;
-import android.media.tv.TvContract;
import android.media.tv.TvInputInfo;
-import android.net.Uri;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.text.Spannable;
@@ -52,12 +49,13 @@ import android.widget.TextView;
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.common.feature.CommonFeatures;
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;
@@ -65,10 +63,6 @@ import com.android.tv.util.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.ImageLoader.LoadTvInputLogoTask;
import com.android.tv.util.Utils;
-import junit.framework.Assert;
-
-import java.util.Objects;
-
/**
* A view to render channel banner.
*/
@@ -98,8 +92,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 +117,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 +125,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private TvContentRating mBlockingContentRating;
private int mLockType;
+ private boolean mUpdateOnTune;
private Animator mResizeAnimator;
private int mCurrentHeight;
@@ -178,24 +174,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
};
- private final ContentObserver mProgramUpdateObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- // TODO: This {@code uri} argument may be a program which is not related to this
- // channel. Consider adding channel id as a parameter of program URI to avoid
- // unnecessary update.
- mHandler.post(mProgramUpdateRunnable);
- }
- };
-
- private final Runnable mProgramUpdateRunnable = new Runnable() {
- @Override
- public void run() {
- removeCallbacks(this);
- updateViews(null);
- }
- };
-
public ChannelBannerView(Context context) {
this(context, null);
}
@@ -243,39 +221,20 @@ 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);
}
}
@Override
- protected void onAttachedToWindow() {
- if (DEBUG) Log.d(TAG, "onAttachedToWindow");
- super.onAttachedToWindow();
- getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI,
- true, mProgramUpdateObserver);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- if (DEBUG) Log.d(TAG, "onDetachedToWindow");
- getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver);
- super.onDetachedFromWindow();
- }
-
- @Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -345,19 +304,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 +329,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;
+ mChannelView.setVisibility(VISIBLE);
+ 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());
+ 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 +374,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 +424,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 +507,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 +517,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 +548,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 +561,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 +588,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 +612,32 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
}
private void updateProgramRatings(Program program) {
- if (mBlockingContentRating != null) {
- mContentRatingsTextViews[0].setText(
- mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating));
- mContentRatingsTextViews[0].setVisibility(View.VISIBLE);
- for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ if (mLockType == LOCK_CHANNEL_INFO) {
+ for (int i = 0; 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 if (mBlockingContentRating != null) {
+ String displayNameForRating =
+ mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating);
+ if (!TextUtils.isEmpty(displayNameForRating)) {
+ mContentRatingsTextViews[0].setText(displayNameForRating);
+ mContentRatingsTextViews[0].setVisibility(View.VISIBLE);
} else {
- mContentRatingsTextViews[i].setText(
- mContentRatingsManager.getDisplayNameForRating(ratings[i]));
- mContentRatingsTextViews[i].setVisibility(View.VISIBLE);
+ mContentRatingsTextViews[0].setVisibility(View.GONE);
+ }
+ for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) {
+ mContentRatingsTextViews[i].setVisibility(View.GONE);
+ }
+ } 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,8 +735,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mLastUpdatedProgram = program;
}
- private void updateBannerHeight(boolean needFadeAnimation) {
- Assert.assertNull(mResizeAnimator);
+ private void updateBannerHeight(boolean needProgramDescriptionFadeAnimation) {
+ SoftPreconditions.checkState(mResizeAnimator == null);
// Need to measure the layout height with the new description text.
CharSequence oldDescription = mProgramDescriptionTextView.getText();
mProgramDescriptionTextView.setText(mProgramDescriptionText);
@@ -785,12 +751,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..dc92111c 100644
--- a/src/com/android/tv/ui/SelectInputView.java
+++ b/src/com/android/tv/ui/SelectInputView.java
@@ -18,7 +18,6 @@ package com.android.tv.ui;
import android.content.Context;
import android.content.res.Resources;
-import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
@@ -37,14 +36,13 @@ import android.widget.TextView;
import com.android.tv.ApplicationSingletons;
import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.analytics.DurationTimer;
+import com.android.tv.util.DurationTimer;
import com.android.tv.analytics.Tracker;
import com.android.tv.data.Channel;
import com.android.tv.util.TvInputManagerHelper;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -58,7 +56,7 @@ public class SelectInputView extends VerticalGridView implements
private final TvInputManagerHelper mTvInputManagerHelper;
private final List<TvInputInfo> mInputList = new ArrayList<>();
- private final InputsComparator mComparator = new InputsComparator();
+ private final TvInputManagerHelper.HardwareInputComparator mComparator;
private final Tracker mTracker;
private final DurationTimer mViewDurationTimer = new DurationTimer();
private final TvInputCallback mTvInputCallback = new TvInputCallback() {
@@ -149,6 +147,8 @@ public class SelectInputView extends VerticalGridView implements
ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
mTracker = appSingletons.getTracker();
mTvInputManagerHelper = appSingletons.getTvInputManagerHelper();
+ mComparator =
+ new TvInputManagerHelper.HardwareInputComparator(context, mTvInputManagerHelper);
Resources resources = context.getResources();
mInputItemHeight = resources.getDimensionPixelSize(R.dimen.input_banner_item_height);
@@ -385,72 +385,6 @@ public class SelectInputView extends VerticalGridView implements
}
}
- private class InputsComparator implements Comparator<TvInputInfo> {
- @Override
- public int compare(TvInputInfo lhs, TvInputInfo rhs) {
- if (lhs == null) {
- return (rhs == null) ? 0 : 1;
- }
- if (rhs == null) {
- return -1;
- }
-
- boolean enabledL = isInputEnabled(lhs);
- boolean enabledR = isInputEnabled(rhs);
- if (enabledL != enabledR) {
- return enabledL ? -1 : 1;
- }
-
- int priorityL = getPriority(lhs);
- int priorityR = getPriority(rhs);
- if (priorityL != priorityR) {
- return priorityR - priorityL;
- }
-
- String customLabelL = (String) lhs.loadCustomLabel(getContext());
- String customLabelR = (String) rhs.loadCustomLabel(getContext());
- if (!TextUtils.equals(customLabelL, customLabelR)) {
- customLabelL = customLabelL == null ? "" : customLabelL;
- customLabelR = customLabelR == null ? "" : customLabelR;
- return customLabelL.compareToIgnoreCase(customLabelR);
- }
-
- String labelL = (String) lhs.loadLabel(getContext());
- String labelR = (String) rhs.loadLabel(getContext());
- labelL = labelL == null ? "" : labelL;
- labelR = labelR == null ? "" : labelR;
- return labelL.compareToIgnoreCase(labelR);
- }
-
- private int getPriority(TvInputInfo info) {
- switch (info.getType()) {
- case TvInputInfo.TYPE_TUNER:
- return 9;
- case TvInputInfo.TYPE_HDMI:
- HdmiDeviceInfo hdmiInfo = info.getHdmiDeviceInfo();
- if (hdmiInfo != null && hdmiInfo.isCecDevice()) {
- return 8;
- }
- return 7;
- case TvInputInfo.TYPE_DVI:
- return 6;
- case TvInputInfo.TYPE_COMPONENT:
- return 5;
- case TvInputInfo.TYPE_SVIDEO:
- return 4;
- case TvInputInfo.TYPE_COMPOSITE:
- return 3;
- case TvInputInfo.TYPE_DISPLAY_PORT:
- return 2;
- case TvInputInfo.TYPE_VGA:
- return 1;
- case TvInputInfo.TYPE_SCART:
- default:
- return 0;
- }
- }
- }
-
/**
* A callback interface for the input selection.
*/
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index cbe459fb..48386698 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -19,9 +19,18 @@ package com.android.tv.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
-import android.annotation.SuppressLint;
+import android.app.Activity;
+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.content.res.Resources;
+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;
@@ -33,12 +42,11 @@ import android.media.tv.TvView.TvInputCallback;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
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;
@@ -47,23 +55,29 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.android.tv.ApplicationSingletons;
+import com.android.tv.Features;
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;
@@ -79,6 +93,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
public static final int VIDEO_UNAVAILABLE_REASON_NOT_TUNED = -1;
public static final int VIDEO_UNAVAILABLE_REASON_NO_RESOURCE = -2;
+ public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
+ public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
@@ -105,17 +121,17 @@ 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;
+ private String mTagetInputId;
private TvInputInfo mInputInfo;
private OnTuneListener mOnTuneListener;
private int mVideoWidth;
@@ -125,7 +141,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private float mVideoDisplayAspectRatio;
private int mAudioChannelCount = StreamInfo.AUDIO_CHANNEL_COUNT_UNKNOWN;
private boolean mHasClosedCaption = false;
- private boolean mVideoAvailable;
private boolean mScreenBlocked;
private OnScreenBlockingChangedListener mOnScreenBlockedListener;
private TvContentRating mBlockedContentRating;
@@ -136,10 +151,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;
@@ -150,21 +163,16 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
private final DurationTimer mChannelViewTimer = new DurationTimer();
private InternetCheckTask mInternetCheckTask;
- // A block screen view which has lock icon with black background.
- // This indicates that user's action is needed to play video.
+ // A block screen view to hide the real TV view underlying. It may be used to enforce parental
+ // control, or hide screen when there's no video available and show appropriate information.
private final BlockScreenView mBlockScreenView;
-
- // 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 mTuningImageColorFilter;
// A spinner view to show buffering status.
private final View mBufferingSpinnerView;
- // A View for fade-in/out animation
private final View mDimScreenView;
+
private int mFadeState = FADED_IN;
private Runnable mActionAfterFade;
@@ -286,21 +294,77 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onVideoAvailable(String inputId) {
- unhideScreenByVideoAvailability();
+ 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();
+ }
+ mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NONE;
+ updateBlockScreenAndMuting();
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) {
- hideScreenByVideoAvailability(inputId, 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 + ")");
+ }
+ mVideoUnavailableReason = reason;
+ if (closePipIfNeeded()) {
+ return;
+ }
+ updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(TunableTvView.this);
}
switch (reason) {
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING:
case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
mTracker.sendChannelVideoUnavailable(mCurrentChannel, reason);
default:
@@ -310,8 +374,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onContentAllowed(String inputId) {
- mBlockScreenForTuneView.setVisibility(View.GONE);
- unblockScreenByContentRating();
+ mBlockedContentRating = null;
+ updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onContentAllowed();
}
@@ -319,7 +383,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public void onContentBlocked(String inputId, TvContentRating rating) {
- blockScreenByContentRating(rating);
+ if (rating != null && rating.equals(mBlockedContentRating)) {
+ return;
+ }
+ mBlockedContentRating = rating;
+ if (closePipIfNeeded()) {
+ return;
+ }
+ updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onContentBlocked();
}
@@ -327,6 +398,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);
}
@@ -361,25 +436,17 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
mTracker = appSingletons.getTracker();
mBlockScreenType = BLOCK_SCREEN_TYPE_NORMAL;
mBlockScreenView = (BlockScreenView) findViewById(R.id.block_screen);
- if (!mCanModifyParentalControls) {
- mBlockScreenView.setImage(R.drawable.ic_message_lock_no_permission);
- mBlockScreenView.setScaleType(ImageView.ScaleType.CENTER);
- } else {
- mBlockScreenView.setImage(R.drawable.ic_message_lock);
- }
- mBlockScreenView.setShrunkenImage(R.drawable.ic_message_lock_preview);
- mBlockScreenView.addFadeOutAnimationListener(new AnimatorListenerAdapter() {
+ mBlockScreenView.addInfoFadeInAnimationListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationEnd(Animator animation) {
+ public void onAnimationStart(Animator animation) {
adjustBlockScreenSpacingAndText();
}
});
- mHideScreenView = (BlockScreenView) findViewById(R.id.hide_screen);
- mHideScreenView.setImageVisibility(false);
mBufferingSpinnerView = findViewById(R.id.buffering_spinner);
- mBlockScreenForTuneView = findViewById(R.id.block_screen_for_tune);
- mDimScreenView = findViewById(R.id.dim);
+ mTuningImageColorFilter = getResources()
+ .getColor(R.color.tvview_block_image_color_filter, null);
+ mDimScreenView = findViewById(R.id.dim_screen);
mDimScreenView.animate().setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -397,27 +464,21 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
});
}
- public void initialize(AppLayerTvView tvView, boolean isPip, int screenHeight,
- int shrunkenTvViewHeight) {
- mTvView = tvView;
+ public void initialize(ProgramDataManager programDataManager,
+ TvInputManagerHelper tvInputManagerHelper) {
+ mTvView = (AppLayerTvView) findViewById(R.id.tv_view);
+ mProgramDataManager = programDataManager;
+ mInputManagerHelper = tvInputManagerHelper;
+ mContentRatingsManager = tvInputManagerHelper.getContentRatingsManager();
+ mParentalControlSettings = tvInputManagerHelper.getParentalControlSettings();
if (mInputSessionManager != null) {
- mTvViewSession = mInputSessionManager.createTvViewSession(tvView, this, mCallback);
+ mTvViewSession = mInputSessionManager.createTvViewSession(mTvView, 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;
}
@@ -431,7 +492,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
} else {
mTvView.tune(inputId, channelUri);
}
- hideScreenByVideoAvailability(inputId, TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
+ updateBlockScreenAndMuting();
}
}
@@ -462,15 +524,16 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
- * Reset TV view.
+ * Resets TV view.
*/
public void reset() {
resetInternal();
- hideScreenByVideoAvailability(null, VIDEO_UNAVAILABLE_REASON_NOT_TUNED);
+ mVideoUnavailableReason = VIDEO_UNAVAILABLE_REASON_NOT_TUNED;
+ updateBlockScreenAndMuting();
}
/**
- * Reset TV view to acquire the recording session.
+ * Resets TV view to acquire the recording session.
*/
public void resetByRecording() {
resetInternal();
@@ -497,6 +560,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;
}
@@ -506,8 +576,9 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
*/
public void onParentalControlChanged(boolean enabled) {
mParentControlEnabled = enabled;
- if (!mParentControlEnabled) {
- mBlockScreenForTuneView.setVisibility(View.GONE);
+ if (!enabled) {
+ // Unblock screen immediately if parental control is turned off
+ updateBlockScreenAndMuting();
}
}
@@ -519,6 +590,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");
}
@@ -541,6 +613,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
&& params.getString(NotificationService.TUNE_PARAMS_RECOMMENDATION_TYPE) != null;
boolean needSurfaceSizeUpdate = false;
if (!inputInfo.equals(mInputInfo)) {
+ mTagetInputId = inputInfo.getId();
mInputInfo = inputInfo;
mCanReceiveInputEvent = getContext().getPackageManager().checkPermission(
PERMISSION_RECEIVE_INPUT_EVENT, mInputInfo.getServiceInfo().packageName)
@@ -560,6 +633,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);
@@ -569,19 +643,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
// So we need to call SurfaceHolder.setFixedSize for the new SurfaceView.
getSurfaceView().getHolder().setFixedSize(mFixedSurfaceWidth, mFixedSurfaceHeight);
}
- hideScreenByVideoAvailability(mInputInfo.getId(),
- TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ mVideoUnavailableReason = TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
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) {
- mBlockScreenForTuneView.setVisibility(View.VISIBLE);
- }
+ updateBlockScreenAndMuting();
if (mOnTuneListener != null) {
mOnTuneListener.onStreamInfoChanged(this);
}
@@ -711,7 +779,13 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
public boolean isVideoAvailable() {
- return mVideoAvailable;
+ return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE;
+ }
+
+ @Override
+ public boolean isVideoOrAudioAvailable() {
+ return mVideoUnavailableReason == VIDEO_UNAVAILABLE_REASON_NONE
+ || mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY;
}
@Override
@@ -747,12 +821,50 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
}
/**
- * Returns if the screen is blocked by {@link #blockScreen()}.
+ * Gets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying
+ * {@link TvView}, which is the actual view to play live TV videos.
+ */
+ public MarginLayoutParams getTvViewLayoutParams() {
+ return (MarginLayoutParams) mTvView.getLayoutParams();
+ }
+
+ /**
+ * Sets {@link android.view.ViewGroup.MarginLayoutParams} of the underlying
+ * {@link TvView}, which is the actual view to play live TV videos.
+ */
+ public void setTvViewLayoutParams(MarginLayoutParams layoutParams) {
+ mTvView.setLayoutParams(layoutParams);
+ }
+
+ /**
+ * Gets the underlying {@link AppLayerTvView}, which is the actual view to play live TV videos.
+ */
+ public TvView getTvView() {
+ return mTvView;
+ }
+
+ /**
+ * Returns if the screen is blocked, either by {@link #blockOrUnblockScreen(boolean)} or because
+ * the content is blocked.
+ */
+ public boolean isBlocked() {
+ return isScreenBlocked() || isContentBlocked();
+ }
+
+ /**
+ * Returns if the screen is blocked by {@link #blockOrUnblockScreen(boolean)}.
*/
public boolean isScreenBlocked() {
return mScreenBlocked;
}
+ /**
+ * Returns {@code true} if the content is blocked, otherwise {@code false}.
+ */
+ public boolean isContentBlocked() {
+ return mBlockedContentRating != null;
+ }
+
public void setOnScreenBlockedListener(OnScreenBlockingChangedListener listener) {
mOnScreenBlockedListener = listener;
}
@@ -766,77 +878,23 @@ 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;
- 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);
+ public void blockOrUnblockScreen(boolean blockOrUnblock) {
+ if (mScreenBlocked == blockOrUnblock) {
+ return;
}
- }
-
- @Override
- public void setLayoutParams(ViewGroup.LayoutParams params) {
- super.setLayoutParams(params);
- if (mTvView != null) {
- copyLayoutParamsToTvView();
+ mScreenBlocked = blockOrUnblock;
+ if (closePipIfNeeded()) {
+ return;
}
- }
-
- private void copyLayoutParamsToTvView() {
- FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
- FrameLayout.LayoutParams tvViewLp = (FrameLayout.LayoutParams) mTvView.getLayoutParams();
- if (tvViewLp.bottomMargin != lp.bottomMargin
- || tvViewLp.topMargin != lp.topMargin
- || tvViewLp.leftMargin != lp.leftMargin
- || tvViewLp.rightMargin != lp.rightMargin
- || 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.topMargin = lp.topMargin;
- tvViewLp.bottomMargin = lp.bottomMargin;
- tvViewLp.rightMargin = lp.rightMargin;
- tvViewLp.gravity = lp.gravity;
- tvViewLp.height = lp.height;
- tvViewLp.width = lp.width;
- mTvView.setLayoutParams(tvViewLp);
+ updateBlockScreenAndMuting();
+ if (mOnScreenBlockedListener != null) {
+ mOnScreenBlockedListener.onScreenBlockingChanged(blockOrUnblock);
}
}
@@ -859,35 +917,67 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* @param type The type of block screen to set.
*/
public void setBlockScreenType(@BlockScreenType int type) {
- // TODO: need to support the transition from NORMAL to SHRUNKEN and vice verse.
if (mBlockScreenType != type) {
mBlockScreenType = type;
- updateBlockScreenUI(true);
+ updateBlockScreen(true);
}
}
- private void updateBlockScreenUI(boolean animation) {
+ private void updateBlockScreen(boolean animation) {
mBlockScreenView.endAnimations();
-
- if (!mScreenBlocked && mBlockedContentRating == null) {
- mBlockScreenView.setVisibility(GONE);
- return;
- }
-
- mBlockScreenView.setVisibility(VISIBLE);
- if (!animation || mBlockScreenType != TunableTvView.BLOCK_SCREEN_TYPE_NO_UI) {
- adjustBlockScreenSpacingAndText();
+ int blockReason = (mScreenBlocked || mBlockedContentRating != null)
+ && mParentControlEnabled ? VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED
+ : mVideoUnavailableReason;
+ if (blockReason != VIDEO_UNAVAILABLE_REASON_NONE) {
+ mBufferingSpinnerView.setVisibility(
+ blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING
+ || blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING ?
+ VISIBLE : GONE);
+ if (!animation) {
+ adjustBlockScreenSpacingAndText();
+ }
+ if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING) {
+ return;
+ }
+ mBlockScreenView.setVisibility(VISIBLE);
+ mBlockScreenView.setBackgroundImage(null);
+ if (blockReason == VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED) {
+ mBlockScreenView.setIconVisibility(true);
+ if (!mCanModifyParentalControls) {
+ mBlockScreenView.setIconImage(R.drawable.ic_message_lock_no_permission);
+ mBlockScreenView.setIconScaleType(ImageView.ScaleType.CENTER);
+ } else {
+ mBlockScreenView.setIconImage(R.drawable.ic_message_lock);
+ mBlockScreenView.setIconScaleType(ImageView.ScaleType.FIT_CENTER);
+ }
+ } else {
+ if (mInternetCheckTask != null) {
+ mInternetCheckTask.cancel(true);
+ mInternetCheckTask = null;
+ }
+ mBlockScreenView.setIconVisibility(false);
+ if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING) {
+ showImageForTuningIfNeeded();
+ } else if (blockReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN
+ && mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
+ mInternetCheckTask = new InternetCheckTask();
+ mInternetCheckTask.execute();
+ }
+ }
+ mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
+ } else {
+ mBufferingSpinnerView.setVisibility(GONE);
+ if (mBlockScreenView.getVisibility() == VISIBLE) {
+ mBlockScreenView.fadeOut();
+ }
}
- mBlockScreenView.onBlockStatusChanged(mBlockScreenType, animation);
}
private void adjustBlockScreenSpacingAndText() {
- // TODO: need to add animation for padding change when the block screen type is changed
- // NORMAL to SHRUNKEN and vice verse.
mBlockScreenView.setSpacing(mBlockScreenType);
String text = getBlockScreenText();
if (text != null) {
- mBlockScreenView.setText(text);
+ mBlockScreenView.setInfoText(text);
}
}
@@ -896,151 +986,121 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
* Note that returning {@code null} value means that the current text should not be changed.
*/
private String getBlockScreenText() {
- if (mScreenBlocked) {
+ // TODO: add a test for this method
+ Resources res = getResources();
+ if (mScreenBlocked && mParentControlEnabled) {
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
return "";
case BLOCK_SCREEN_TYPE_NORMAL:
if (mCanModifyParentalControls) {
- return getResources().getString(R.string.tvview_channel_locked);
+ return res.getString(R.string.tvview_channel_locked);
} else {
- return getResources().getString(
- R.string.tvview_channel_locked_no_permission);
+ return res.getString(R.string.tvview_channel_locked_no_permission);
}
}
- } else if (mBlockedContentRating != null) {
+ } else if (mBlockedContentRating != null && mParentControlEnabled) {
String name = mContentRatingsManager.getDisplayNameForRating(mBlockedContentRating);
switch (mBlockScreenType) {
case BLOCK_SCREEN_TYPE_NO_UI:
return "";
case BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW:
if (TextUtils.isEmpty(name)) {
- return getResources().getString(R.string.shrunken_tvview_content_locked);
+ return res.getString(R.string.shrunken_tvview_content_locked);
+ } else if (name.equals(res.getString(R.string.unrated_rating_name))) {
+ return res.getString(R.string.shrunken_tvview_content_locked_unrated);
} else {
- return getContext().getString(
- R.string.shrunken_tvview_content_locked_format, name);
+ return res.getString(R.string.shrunken_tvview_content_locked_format, name);
}
case BLOCK_SCREEN_TYPE_NORMAL:
if (TextUtils.isEmpty(name)) {
if (mCanModifyParentalControls) {
- return getResources().getString(R.string.tvview_content_locked);
+ return res.getString(R.string.tvview_content_locked);
} else {
- return getResources().getString(
- R.string.tvview_content_locked_no_permission);
+ return res.getString(R.string.tvview_content_locked_no_permission);
}
} else {
if (mCanModifyParentalControls) {
- return getContext().getString(
- R.string.tvview_content_locked_format, name);
+ return name.equals(res.getString(R.string.unrated_rating_name))
+ ? res.getString(R.string.tvview_content_locked_unrated)
+ : res.getString(R.string.tvview_content_locked_format, name);
} else {
- return getContext().getString(
- R.string.tvview_content_locked_format_no_permission, name);
+ return name.equals(res.getString(R.string.unrated_rating_name))
+ ? res.getString(
+ R.string.tvview_content_locked_unrated_no_permission)
+ : res.getString(
+ R.string.tvview_content_locked_format_no_permission,
+ name);
}
}
}
+ } else if (mVideoUnavailableReason != VIDEO_UNAVAILABLE_REASON_NONE) {
+ switch (mVideoUnavailableReason) {
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
+ return res.getString(R.string.tvview_msg_audio_only);
+ case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL:
+ return res.getString(R.string.tvview_msg_weak_signal);
+ case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
+ return getTuneConflictMessage();
+ default:
+ return "";
+ }
}
return null;
}
- 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);
- }
+ private boolean closePipIfNeeded() {
+ if (Features.PICTURE_IN_PICTURE.isEnabled(getContext())
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && ((Activity) getContext()).isInPictureInPictureMode()
+ && (mScreenBlocked
+ || mBlockedContentRating != null
+ || mVideoUnavailableReason
+ == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN)) {
+ ((Activity) getContext()).finish();
+ return true;
}
+ return false;
}
- public void unblockScreen() {
- mScreenBlocked = false;
- checkBlockScreenAndMuteNeeded();
- if (mOnScreenBlockedListener != null) {
- mOnScreenBlockedListener.onScreenBlockingChanged(false);
- }
+ private void updateBlockScreenAndMuting() {
+ updateBlockScreen(false);
+ updateMuteStatus();
}
- private void unblockScreenByContentRating() {
- mBlockedContentRating = null;
- checkBlockScreenAndMuteNeeded();
+ private boolean shouldShowImageForTuning() {
+ if (mVideoUnavailableReason != TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING
+ || mScreenBlocked || mBlockedContentRating != null || mCurrentChannel == null
+ || mIsUnderShrunken || getWidth() == 0 || getWidth() == 0 || !isBundledInput()) {
+ return false;
+ }
+ Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
+ if (currentProgram == null) {
+ return false;
+ }
+ TvContentRating rating =
+ mParentalControlSettings.getBlockedRating(currentProgram.getContentRatings());
+ return !(mParentControlEnabled && rating != null);
}
- @UiThread
- private void hideScreenByVideoAvailability(String inputId, int reason) {
- mVideoAvailable = false;
- mVideoUnavailableReason = reason;
- if (mInternetCheckTask != null) {
- mInternetCheckTask.cancel(true);
- mInternetCheckTask = null;
- }
- switch (reason) {
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(R.string.tvview_msg_audio_only);
- mBufferingSpinnerView.setVisibility(GONE);
- unmuteIfPossible();
- 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);
- mBufferingSpinnerView.setVisibility(GONE);
- mute();
- break;
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(null);
- mBufferingSpinnerView.setVisibility(VISIBLE);
- mute();
- break;
- case VIDEO_UNAVAILABLE_REASON_NOT_TUNED:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(null);
- mBufferingSpinnerView.setVisibility(GONE);
- mute();
- break;
- case VIDEO_UNAVAILABLE_REASON_NO_RESOURCE:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(getTuneConflictMessage(inputId));
- mBufferingSpinnerView.setVisibility(GONE);
- mute();
- break;
- case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN:
- default:
- mHideScreenView.setVisibility(VISIBLE);
- mHideScreenView.setImageVisibility(false);
- mHideScreenView.setText(null);
- mBufferingSpinnerView.setVisibility(GONE);
- mute();
- if (mCurrentChannel != null && !mCurrentChannel.isPhysicalTunerChannel()) {
- mInternetCheckTask = new InternetCheckTask();
- mInternetCheckTask.execute();
- }
- break;
+ private void showImageForTuningIfNeeded() {
+ if (shouldShowImageForTuning()) {
+ if (mCurrentChannel == null) {
+ return;
+ }
+ Program currentProgram = mProgramDataManager.getCurrentProgram(mCurrentChannel.getId());
+ if (currentProgram != null) {
+ currentProgram.loadPosterArt(getContext(), getWidth(), getHeight(),
+ createProgramPosterArtCallback(mCurrentChannel.getId()));
+ }
}
}
- private String getTuneConflictMessage(String inputId) {
- if (inputId != null) {
- TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(inputId);
+ private String getTuneConflictMessage() {
+ if (mTagetInputId != null) {
+ TvInputInfo input = mInputManager.getTvInputInfo(mTagetInputId);
+ Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(mTagetInputId);
if (timeMs != null) {
return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource,
input.getTunerCount(),
@@ -1050,27 +1110,36 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
return null;
}
- private void unhideScreenByVideoAvailability() {
- mVideoAvailable = true;
- mHideScreenView.setVisibility(GONE);
- mBufferingSpinnerView.setVisibility(GONE);
- unmuteIfPossible();
- }
-
- private void unmuteIfPossible() {
- if (mVideoAvailable && !mScreenBlocked && mBlockedContentRating == null) {
- unmute();
+ 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 || isVideoOrAudioAvailable()) && !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 +1337,24 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
return mTimeShiftCurrentPositionMs;
}
+ private ImageLoader.ImageLoaderCallback<BlockScreenView> createProgramPosterArtCallback(
+ final long channelId) {
+ return new ImageLoader.ImageLoaderCallback<BlockScreenView>(mBlockScreenView) {
+ @Override
+ public void onBitmapLoaded(BlockScreenView view, @Nullable Bitmap posterArt) {
+ if (posterArt == null || getCurrentChannel() == null
+ || channelId != getCurrentChannel().getId()
+ || !shouldShowImageForTuning()) {
+ return;
+ }
+ Drawable drawablePosterArt = new BitmapDrawable(view.getResources(), posterArt);
+ drawablePosterArt.mutate().setColorFilter(
+ mTuningImageColorFilter, PorterDuff.Mode.SRC_OVER);
+ view.setBackgroundImage(drawablePosterArt);
+ }
+ };
+ }
+
/**
* Used to receive the time-shift events.
*/
@@ -1304,11 +1391,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo {
@Override
protected void onPostExecute(Boolean networkAvailable) {
mInternetCheckTask = null;
- if (!mVideoAvailable && !networkAvailable && isAttachedToWindow()
+ if (!networkAvailable && isAttachedToWindow()
+ && !mScreenBlocked && mBlockedContentRating == null
&& mVideoUnavailableReason == TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN) {
- mHideScreenView.setImageVisibility(true);
- mHideScreenView.setImage(R.drawable.ic_sad_cloud);
- mHideScreenView.setText(R.string.tvview_msg_no_internet_connection);
+ mBlockScreenView.setIconVisibility(true);
+ mBlockScreenView.setIconImage(R.drawable.ic_sad_cloud);
+ mBlockScreenView.setInfoText(R.string.tvview_msg_no_internet_connection);
}
}
}
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index e14b286b..9324742e 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -20,6 +20,7 @@ import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentManager.OnBackStackChangedListener;
import android.content.Intent;
+import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
import android.os.Bundle;
import android.os.Handler;
@@ -39,6 +40,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,14 +48,16 @@ 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.license.LicenseDialogFragment;
import com.android.tv.menu.Menu;
import com.android.tv.menu.Menu.MenuShowReason;
import com.android.tv.menu.MenuRowFactory;
@@ -62,7 +66,6 @@ import com.android.tv.onboarding.NewSourcesFragment;
import com.android.tv.onboarding.SetupSourcesFragment;
import com.android.tv.search.ProgramGuideSearchFragment;
import com.android.tv.ui.TvTransitionManager.SceneType;
-import com.android.tv.ui.sidepanel.SettingsFragment;
import com.android.tv.ui.sidepanel.SideFragmentManager;
import com.android.tv.ui.sidepanel.parentalcontrols.RatingsFragment;
import com.android.tv.util.TvInputManagerHelper;
@@ -157,15 +160,47 @@ public class TvOverlayManager {
// Used for the padded print of the overlay type.
private static final int NUM_OVERLAY_TYPES = 9;
+ @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_UPDATE_STREAM_INFO})
+ private @interface ChannelBannerUpdateReason {}
+ /**
+ * Updates channel banner because the channel banner is forced to show.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1;
+ /**
+ * Updates channel banner because of tuning.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2;
+ /**
+ * Updates channel banner because of fast tuning.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3;
+ /**
+ * Updates channel banner because of info updating.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4;
+ /**
+ * Updates channel banner because the current watched channel is locked or unlocked.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5;
+ /**
+ * Updates channel banner because of stream info updating.
+ */
+ public static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_STREAM_INFO = 6;
+
private static final String FRAGMENT_TAG_SETUP_SOURCES = "tag_setup_sources";
private static final String FRAGMENT_TAG_NEW_SOURCES = "tag_new_sources";
private static final Set<String> AVAILABLE_DIALOG_TAGS = new HashSet<>();
static {
AVAILABLE_DIALOG_TAGS.add(RecentlyWatchedDialogFragment.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(DvrHistoryDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(PinDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(FullscreenDialogFragment.DIALOG_TAG);
- AVAILABLE_DIALOG_TAGS.add(SettingsFragment.LicenseActionItem.DIALOG_TAG);
+ AVAILABLE_DIALOG_TAGS.add(LicenseDialogFragment.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(RatingsFragment.AttributionItem.DIALOG_TAG);
AVAILABLE_DIALOG_TAGS.add(HalfSizedDialogFragment.DIALOG_TAG);
}
@@ -176,8 +211,10 @@ public class TvOverlayManager {
private final ChannelDataManager mChannelDataManager;
private final TvInputManagerHelper mInputManager;
private final Menu mMenu;
+ private final TunableTvView mTvView;
private final SideFragmentManager mSideFragmentManager;
private final ProgramGuide mProgramGuide;
+ private final ChannelBannerView mChannelBannerView;
private final KeypadChannelSwitchView mKeypadChannelSwitchView;
private final SelectInputView mSelectInputView;
private final ProgramGuideSearchFragment mSearchFragment;
@@ -185,6 +222,7 @@ public class TvOverlayManager {
private SafeDismissDialogFragment mCurrentDialog;
private boolean mSetupFragmentActive;
private boolean mNewSourcesFragmentActive;
+ private boolean mChannelBannerHiddenBySideFragment;
private final Handler mHandler = new TvOverlayHandler(this);
private @TvOverlayType int mOpenedOverlays;
@@ -195,15 +233,17 @@ 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);
mChannelDataManager = singletons.getChannelDataManager();
mInputManager = singletons.getTvInputManagerHelper();
+ mTvView = tvView;
+ mChannelBannerView = channelBannerView;
mKeypadChannelSwitchView = keypadChannelSwitchView;
mSelectInputView = selectInputView;
mSearchFragment = searchFragment;
@@ -225,7 +265,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) {
@@ -249,7 +290,7 @@ public class TvOverlayManager {
new Runnable() {
@Override
public void run() {
- mMainActivity.showChannelBannerIfHiddenBySideFragment();
+ showChannelBannerIfHiddenBySideFragment();
onOverlayClosed(OVERLAY_TYPE_SIDE_FRAGMENT);
}
});
@@ -320,6 +361,9 @@ public class TvOverlayManager {
public void release() {
mMenu.release();
mHandler.removeCallbacksAndMessages(null);
+ if (mKeypadChannelSwitchView != null) {
+ mKeypadChannelSwitchView.setChannels(null);
+ }
}
/**
@@ -436,6 +480,13 @@ public class TvOverlayManager {
onOverlayOpened(OVERLAY_TYPE_DIALOG);
}
+ /**
+ * Should be called by {@link MainActivity} when the currently browsable channels are updated.
+ */
+ public void onBrowsableChannelsUpdated() {
+ mKeypadChannelSwitchView.setChannels(mChannelTuner.getBrowsableChannelList());
+ }
+
private void runAfterSideFragmentsAreClosed(final Runnable runnable) {
if (mSideFragmentManager.isSidePanelVisible()) {
// When the side panel is closing, it closes all the fragments, so the new fragment
@@ -541,7 +592,7 @@ public class TvOverlayManager {
* Shows DVR manager.
*/
public void showDvrManager() {
- Intent intent = new Intent(mMainActivity, DvrActivity.class);
+ Intent intent = new Intent(mMainActivity, DvrBrowseActivity.class);
mMainActivity.startActivity(intent);
}
@@ -564,18 +615,34 @@ public class TvOverlayManager {
}
/**
+ * Shows DVR history dialog.
+ */
+ public void showDvrHistoryDialog() {
+ showDialogFragment(DvrHistoryDialogFragment.DIALOG_TAG,
+ new DvrHistoryDialogFragment(), false);
+ }
+
+ /**
* Shows banner view.
*/
public void showBanner() {
mTransitionManager.goToChannelBannerScene();
}
- public void showKeypadChannelSwitch() {
- hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
- mTransitionManager.goToKeypadChannelSwitchScene();
+ /**
+ * Pops up the KeypadChannelSwitchView with the given key input event.
+ *
+ * @param keyCode A key code of the key event.
+ */
+ public void showKeypadChannelSwitch(int keyCode) {
+ if (mChannelTuner.areAllChannelsLoaded()) {
+ hideOverlays(TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SCENE
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
+ mTransitionManager.goToKeypadChannelSwitchScene();
+ mKeypadChannelSwitchView.onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
+ }
}
/**
@@ -619,6 +686,31 @@ public class TvOverlayManager {
}
/**
+ * Shows/hides the program guide according to it's hidden or shown now.
+ *
+ * @return {@code true} if program guide is going to be shown, otherwise {@code false}.
+ */
+ public boolean toggleProgramGuide() {
+ if (mProgramGuide.isActive()) {
+ mProgramGuide.onBackPressed();
+ return false;
+ } else {
+ showProgramGuide();
+ return true;
+ }
+ }
+
+ /**
+ * Sets blocking content rating of the currently playing TV channel.
+ */
+ public void setBlockingContentRating(TvContentRating rating) {
+ if (!mMainActivity.isChannelChangeKeyDownReceived()) {
+ mChannelBannerView.setBlockingContentRating(rating);
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK);
+ }
+ }
+
+ /**
* Hides all the opened overlays according to the flags.
*/
// TODO: Add test for this method.
@@ -631,12 +723,12 @@ public class TvOverlayManager {
} else {
if (mCurrentDialog != null) {
if (mCurrentDialog instanceof PinDialogFragment) {
- // The result listener of PinDialogFragment could call MenuView when
- // the dialog is dismissed. In order not to call it, set the result listener
- // to null.
- ((PinDialogFragment) mCurrentDialog).setResultListener(null);
+ // We don't want any OnPinCheckedListener is triggered to prevent any possible
+ // side effects. Dismisses the dialog silently.
+ ((PinDialogFragment) mCurrentDialog).dismissSilently();
+ } else {
+ mCurrentDialog.dismiss();
}
- mCurrentDialog.dismiss();
}
mPendingDialogActionQueue.clear();
mCurrentDialog = null;
@@ -674,7 +766,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 {
@@ -701,6 +793,83 @@ public class TvOverlayManager {
|| mNewSourcesFragmentActive;
}
+ /**
+ * Updates and shows channel banner if it's needed.
+ */
+ public void updateChannelBannerAndShowIfNeeded(@ChannelBannerUpdateReason int reason) {
+ if(DEBUG) Log.d(TAG, "updateChannelBannerAndShowIfNeeded(reason=" + reason + ")");
+ if (mMainActivity.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 (reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST) {
+ if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled()
+ && mMainActivity.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 (mMainActivity.getParentalControlSettings().isParentalControlsEnabled()) {
+ if (mMainActivity.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.isContentBlocked() || (mMainActivity.getParentalControlSettings()
+ .isParentalControlsEnabled() && !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 lock type is not changed, we don't need to update channel banner by parental
+ // control.
+ 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);
+ }
+ }
+ boolean needToShowBanner = (reason == UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW
+ || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE
+ || reason == UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST);
+ if (needToShowBanner && !mMainActivity.willShowOverlayUiWhenResume()
+ && getCurrentDialog() == null
+ && !isSetupFragmentActive()
+ && !isNewSourcesFragmentActive()) {
+ if (mChannelTuner.getCurrentChannel() == null) {
+ mChannelBannerHiddenBySideFragment = false;
+ } else if (getSideFragmentManager().isActive()) {
+ mChannelBannerHiddenBySideFragment = true;
+ } else {
+ mChannelBannerHiddenBySideFragment = false;
+ showBanner();
+ }
+ }
+ }
+
@TvOverlayType private int convertSceneToOverlayType(@SceneType int sceneType) {
switch (sceneType) {
case TvTransitionManager.SCENE_TYPE_CHANNEL_BANNER:
@@ -749,6 +918,18 @@ public class TvOverlayManager {
}
}
+ /**
+ * Shows the channel banner if it was hidden from the side fragment.
+ *
+ * <p>When the side fragment is visible, showing the channel banner should be put off until the
+ * side fragment is closed even though the channel changes.
+ */
+ private void showChannelBannerIfHiddenBySideFragment() {
+ if (mChannelBannerHiddenBySideFragment) {
+ updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW);
+ }
+ }
+
private String toBinaryString(int value) {
return String.format("0b%" + NUM_OVERLAY_TYPES + "s", Integer.toBinaryString(value))
.replace(' ', '0');
@@ -843,6 +1024,7 @@ public class TvOverlayManager {
timeShiftManager.play();
showMenu(Menu.REASON_PLAY_CONTROLS_PLAY);
break;
+ case KeyEvent.KEYCODE_MEDIA_STOP:
case KeyEvent.KEYCODE_MEDIA_PAUSE:
timeShiftManager.pause();
showMenu(Menu.REASON_PLAY_CONTROLS_PAUSE);
@@ -916,7 +1098,7 @@ public class TvOverlayManager {
}
if (mMenu.isActive()) {
if (KeypadChannelSwitchView.isChannelNumberKey(keyCode)) {
- mMainActivity.showKeypadChannelSwitchView(keyCode);
+ showKeypadChannelSwitch(keyCode);
return MainActivity.KEY_EVENT_HANDLER_RESULT_HANDLED;
}
return MainActivity.KEY_EVENT_HANDLER_RESULT_DISPATCH_TO_OVERLAY;
@@ -971,6 +1153,7 @@ public class TvOverlayManager {
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
+ case KeyEvent.KEYCODE_MEDIA_STOP:
return true;
}
return false;
diff --git a/src/com/android/tv/ui/TvTransitionManager.java b/src/com/android/tv/ui/TvTransitionManager.java
index 52e96cc0..628bbb72 100644
--- a/src/com/android/tv/ui/TvTransitionManager.java
+++ b/src/com/android/tv/ui/TvTransitionManager.java
@@ -129,8 +129,8 @@ public class TvTransitionManager extends TransitionManager {
public void goToSelectInputScene() {
initIfNeeded();
if (mCurrentScene != mSelectInputScene) {
- transitionTo(mSelectInputScene);
mSelectInputView.setCurrentChannel(mMainActivity.getCurrentChannel());
+ transitionTo(mSelectInputScene);
}
}
diff --git a/src/com/android/tv/ui/TvViewUiManager.java b/src/com/android/tv/ui/TvViewUiManager.java
index bf874fc7..f042987a 100644
--- a/src/com/android/tv/ui/TvViewUiManager.java
+++ b/src/com/android/tv/ui/TvViewUiManager.java
@@ -24,21 +24,19 @@ import android.animation.TimeInterpolator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Point;
import android.hardware.display.DisplayManager;
+import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Property;
import android.view.Display;
-import android.view.Gravity;
-import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
@@ -52,9 +50,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,42 +66,44 @@ 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;
- private final Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch(msg.what) {
- case MSG_SET_LAYOUT_PARAMS:
- FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) msg.obj;
- if (DEBUG) {
- Log.d(TAG, "setFixedSize: w=" + layoutParams.width + " h="
- + layoutParams.height);
- }
- mTvView.setLayoutParams(layoutParams);
- // Smooth PIP size change, we don't change surface size when
- // isInPictureInPictureMode is true.
- if (!Features.PICTURE_IN_PICTURE.isEnabled(mContext)
- || !((Activity) mContext).isInPictureInPictureMode()) {
- mTvView.setFixedSurfaceSize(layoutParams.width, layoutParams.height);
+ private final Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SET_LAYOUT_PARAMS:
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) msg.obj;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "setFixedSize: w="
+ + layoutParams.width
+ + " h="
+ + layoutParams.height);
+ }
+ mTvView.setTvViewLayoutParams(layoutParams);
+ mTvView.setLayoutParams(mTvViewFrame);
+ // Smooth PIP size change, we don't change surface size when
+ // isInPictureInPictureMode is true.
+ if (!Features.PICTURE_IN_PICTURE.isEnabled(mContext)
+ || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+ && !((Activity) mContext).isInPictureInPictureMode())) {
+ mTvView.setFixedSurfaceSize(
+ layoutParams.width, layoutParams.height);
+ }
+ break;
}
- break;
- }
- }
- };
+ }
+ };
private int mDisplayMode;
// Used to restore the previous state from ShrunkenTvView state.
private int mTvViewStartMarginBeforeShrunken;
@@ -113,16 +112,13 @@ 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
// to TV view's position.
- private MarginLayoutParams mTvViewFrame;
- private MarginLayoutParams mLastAnimatedTvViewFrame;
- private MarginLayoutParams mOldTvViewFrame;
+ private FrameLayout.LayoutParams mTvViewFrame;
+ private FrameLayout.LayoutParams mLastAnimatedTvViewFrame;
+ private FrameLayout.LayoutParams mOldTvViewFrame;
private ObjectAnimator mBackgroundAnimator;
private int mBackgroundColor;
private int mAppliedDisplayedMode = DisplayMode.MODE_NOT_DEFINED;
@@ -130,12 +126,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 +142,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 +156,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 +184,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 +198,7 @@ public class TvViewUiManager {
*/
public void endShrunkenTvView() {
mIsUnderShrunkenTvView = false;
+ mTvView.setIsUnderShrunken(false);
setTvViewMargin(mTvViewStartMarginBeforeShrunken, mTvViewEndMarginBeforeShrunken);
setDisplayMode(mDisplayModeBeforeShrunken, false, true);
}
@@ -296,9 +274,9 @@ public class TvViewUiManager {
}
/**
- * Updates TvView. It is called when video resolution is updated.
+ * Updates TvView's aspect ratio. It should be called when video resolution is changed.
*/
- public void updateTvView() {
+ public void updateTvAspectRatio() {
applyDisplayMode(mTvView.getVideoDisplayAspectRatio(), false, false);
if (mTvView.isVideoAvailable() && mTvView.isFadedOut()) {
mTvView.fadeIn(mResources.getInteger(R.integer.tvview_fade_in_duration),
@@ -327,120 +305,6 @@ public class TvViewUiManager {
}
/**
- * Returns the current PIP layout. The layout should be one of
- * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
- * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
- * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
- */
- public int getPipLayout() {
- return mPipLayout;
- }
-
- /**
- * Sets the PIP layout. The layout should be one of
- * {@link TvSettings#PIP_LAYOUT_BOTTOM_RIGHT}, {@link TvSettings#PIP_LAYOUT_TOP_RIGHT},
- * {@link TvSettings#PIP_LAYOUT_TOP_LEFT}, {@link TvSettings#PIP_LAYOUT_BOTTOM_LEFT} and
- * {@link TvSettings#PIP_LAYOUT_SIDE_BY_SIDE}.
- *
- * @param storeInPreference if true, the stored value will be restored by
- * {@link #restorePipLayout()}.
- */
- public void setPipLayout(int pipLayout, boolean storeInPreference) {
- mPipLayout = pipLayout;
- if (storeInPreference) {
- TvSettings.setPipLayout(mContext, pipLayout);
- }
- updatePipView(mTvViewFrame);
- if (mPipLayout == TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- setTvViewMargin(mTvViewPapStartMargin, mTvViewPapEndMargin);
- setDisplayMode(DisplayMode.MODE_NORMAL, false, false);
- } else {
- setTvViewMargin(0, 0);
- restoreDisplayMode(false);
- }
- mTvOptionsManager.onPipLayoutChanged(pipLayout);
- }
-
- /**
- * Restores the PIP layout which {@link #setPipLayout} lastly stores.
- */
- public void restorePipLayout() {
- setPipLayout(TvSettings.getPipLayout(mContext), false);
- }
-
- /**
- * Called when PIP is started.
- */
- public void onPipStart() {
- mPipStarted = true;
- updatePipView();
- mPipView.setVisibility(View.VISIBLE);
- }
-
- /**
- * Called when PIP is stopped.
- */
- public void onPipStop() {
- setTvViewMargin(0, 0);
- mPipView.setVisibility(View.GONE);
- mPipStarted = false;
- }
-
- /**
- * Called when PIP is resumed.
- */
- public void showPipForResume() {
- mPipView.setVisibility(View.VISIBLE);
- }
-
- /**
- * Called when PIP is paused.
- */
- public void hidePipForPause() {
- if (mPipLayout != TvSettings.PIP_LAYOUT_SIDE_BY_SIDE) {
- mPipView.setVisibility(View.GONE);
- }
- }
-
- /**
- * Updates PIP view. It is usually called, when video resolution in PIP is updated.
- */
- public void updatePipView() {
- updatePipView(mTvViewFrame);
- }
-
- /**
- * Returns the size of the PIP view.
- */
- public int getPipSize() {
- return mPipSize;
- }
-
- /**
- * Sets PIP size and applies it immediately.
- *
- * @param pipSize PIP size. The value should be one of {@link TvSettings#PIP_SIZE_BIG}
- * and {@link TvSettings#PIP_SIZE_SMALL}.
- * @param storeInPreference if true, the stored value will be restored by
- * {@link #restorePipSize()}.
- */
- public void setPipSize(int pipSize, boolean storeInPreference) {
- mPipSize = pipSize;
- if (storeInPreference) {
- TvSettings.setPipSize(mContext, pipSize);
- }
- updatePipView(mTvViewFrame);
- mTvOptionsManager.onPipSizeChanged(pipSize);
- }
-
- /**
- * Restores the PIP size which {@link #setPipSize} lastly stores.
- */
- public void restorePipSize() {
- setPipSize(TvSettings.getPipSize(mContext), false);
- }
-
- /**
* This margins will be applied when applyDisplayMode is called.
*/
private void setTvViewMargin(int tvViewStartMargin, int tvViewEndMargin) {
@@ -488,14 +352,14 @@ public class TvViewUiManager {
}
private void setTvViewPosition(final FrameLayout.LayoutParams layoutParams,
- MarginLayoutParams tvViewFrame, boolean animate) {
+ FrameLayout.LayoutParams tvViewFrame, boolean animate) {
if (DEBUG) {
Log.d(TAG, "setTvViewPosition: w=" + layoutParams.width + " h=" + layoutParams.height
+ " s=" + layoutParams.getMarginStart() + " t=" + layoutParams.topMargin
+ " e=" + layoutParams.getMarginEnd() + " b=" + layoutParams.bottomMargin
+ " animate=" + animate);
}
- MarginLayoutParams oldTvViewFrame = mTvViewFrame;
+ FrameLayout.LayoutParams oldTvViewFrame = mTvViewFrame;
mTvViewLayoutParams = layoutParams;
mTvViewFrame = tvViewFrame;
if (animate) {
@@ -503,11 +367,11 @@ public class TvViewUiManager {
if (mTvViewAnimator.isStarted()) {
// Cancel the current animation and start new one.
mTvViewAnimator.cancel();
- mOldTvViewFrame = mLastAnimatedTvViewFrame;
+ mOldTvViewFrame = new FrameLayout.LayoutParams(mLastAnimatedTvViewFrame);
} else {
- mOldTvViewFrame = oldTvViewFrame;
+ mOldTvViewFrame = new FrameLayout.LayoutParams(oldTvViewFrame);
}
- mTvViewAnimator.setObjectValues(mTvView.getLayoutParams(), layoutParams);
+ mTvViewAnimator.setObjectValues(mTvView.getTvViewLayoutParams(), layoutParams);
mTvViewAnimator.setEvaluator(new TypeEvaluator<FrameLayout.LayoutParams>() {
FrameLayout.LayoutParams lp;
@Override
@@ -517,7 +381,7 @@ public class TvViewUiManager {
lp = new FrameLayout.LayoutParams(0, 0);
lp.gravity = startValue.gravity;
}
- interpolateMarginsRelative(lp, startValue, endValue, fraction);
+ interpolateMargins(lp, startValue, endValue, fraction);
return lp;
}
});
@@ -538,116 +402,10 @@ public class TvViewUiManager {
mHandler.removeMessages(MSG_SET_LAYOUT_PARAMS);
mHandler.obtainMessage(MSG_SET_LAYOUT_PARAMS, layoutParams).sendToTarget();
} 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);
+ mTvView.setTvViewLayoutParams(layoutParams);
+ mTvView.setLayoutParams(mTvViewFrame);
}
}
-
- 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);
- }
}
private void initTvAnimatorIfNeeded() {
@@ -663,7 +421,7 @@ public class TvViewUiManager {
// because TvView may request layout itself during animation and layout SurfaceView with
// its own parameters when TvInputService requests to do so.
mTvViewAnimator = new ObjectAnimator();
- mTvViewAnimator.setTarget(mTvView);
+ mTvViewAnimator.setTarget(mTvView.getTvView());
mTvViewAnimator.setProperty(
Property.of(FrameLayout.class, ViewGroup.LayoutParams.class, "layoutParams"));
mTvViewAnimator.setDuration(mResources.getInteger(R.integer.tvview_anim_duration));
@@ -693,10 +451,10 @@ public class TvViewUiManager {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
float fraction = animator.getAnimatedFraction();
- mLastAnimatedTvViewFrame = new MarginLayoutParams(0, 0);
- interpolateMarginsRelative(mLastAnimatedTvViewFrame,
+ mLastAnimatedTvViewFrame = (FrameLayout.LayoutParams) mTvView.getLayoutParams();
+ interpolateMargins(mLastAnimatedTvViewFrame,
mOldTvViewFrame, mTvViewFrame, fraction);
- updatePipView(mLastAnimatedTvViewFrame);
+ mTvView.setLayoutParams(mLastAnimatedTvViewFrame);
}
});
}
@@ -745,66 +503,58 @@ public class TvViewUiManager {
}
int availableAreaWidth = mWindowWidth - mTvViewStartMargin - mTvViewEndMargin;
int availableAreaHeight = availableAreaWidth * mWindowHeight / mWindowWidth;
- FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0,
- ((FrameLayout.LayoutParams) mTvView.getLayoutParams()).gravity);
int displayMode = mDisplayMode;
- double availableAreaRatio = 0;
- double videoRatio = 0;
+ float availableAreaRatio = 0;
if (availableAreaWidth <= 0 || availableAreaHeight <= 0) {
displayMode = DisplayMode.MODE_FULL;
Log.w(TAG, "Some resolution info is missing during applyDisplayMode. ("
+ "availableAreaWidth=" + availableAreaWidth + ", availableAreaHeight="
+ availableAreaHeight + ")");
} else {
- availableAreaRatio = (double) availableAreaWidth / availableAreaHeight;
- videoRatio = videoDisplayAspectRatio;
+ availableAreaRatio = (float) availableAreaWidth / availableAreaHeight;
}
-
- int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2;
- MarginLayoutParams tvViewFrame = createMarginLayoutParams(
- mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop);
- layoutParams.width = availableAreaWidth;
- layoutParams.height = availableAreaHeight;
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(0, 0,
+ ((FrameLayout.LayoutParams) mTvView.getTvViewLayoutParams()).gravity);
switch (displayMode) {
- case DisplayMode.MODE_FULL:
- layoutParams.width = availableAreaWidth;
- layoutParams.height = availableAreaHeight;
- break;
case DisplayMode.MODE_ZOOM:
- if (videoRatio < availableAreaRatio) {
+ if (videoDisplayAspectRatio < availableAreaRatio) {
// Y axis will be clipped.
layoutParams.width = availableAreaWidth;
- layoutParams.height = (int) Math.round(availableAreaWidth / videoRatio);
+ layoutParams.height = Math.round(availableAreaWidth / videoDisplayAspectRatio);
} else {
// X axis will be clipped.
- layoutParams.width = (int) Math.round(availableAreaHeight * videoRatio);
+ layoutParams.width = Math.round(availableAreaHeight * videoDisplayAspectRatio);
layoutParams.height = availableAreaHeight;
}
break;
case DisplayMode.MODE_NORMAL:
- if (videoRatio < availableAreaRatio) {
+ if (videoDisplayAspectRatio < availableAreaRatio) {
// X axis has black area.
- layoutParams.width = (int) Math.round(availableAreaHeight * videoRatio);
+ layoutParams.width = Math.round(availableAreaHeight * videoDisplayAspectRatio);
layoutParams.height = availableAreaHeight;
} else {
// Y axis has black area.
layoutParams.width = availableAreaWidth;
- layoutParams.height = (int) Math.round(availableAreaWidth / videoRatio);
+ layoutParams.height = Math.round(availableAreaWidth / videoDisplayAspectRatio);
}
break;
+ case DisplayMode.MODE_FULL:
+ default:
+ layoutParams.width = availableAreaWidth;
+ layoutParams.height = availableAreaHeight;
+ break;
}
-
// FrameLayout has an issue with centering when left and right margins differ.
// So stick to Gravity.START | Gravity.CENTER_VERTICAL.
- int marginStart = mTvViewStartMargin + (availableAreaWidth - layoutParams.width) / 2;
+ int marginStart = (availableAreaWidth - layoutParams.width) / 2;
layoutParams.setMarginStart(marginStart);
- // Set marginEnd as well because setTvViewPosition uses both start/end margin.
- layoutParams.setMarginEnd(mWindowWidth - layoutParams.width - marginStart);
-
+ int tvViewFrameTop = (mWindowHeight - availableAreaHeight) / 2;
+ FrameLayout.LayoutParams tvViewFrame = createMarginLayoutParams(
+ mTvViewStartMargin, mTvViewEndMargin, tvViewFrameTop, tvViewFrameTop);
+ setTvViewPosition(layoutParams, tvViewFrame, animate);
setBackgroundColor(mResources.getColor(isTvViewFullScreen()
? R.color.tvactivity_background : R.color.tvactivity_background_on_shrunken_tvview,
- null), layoutParams, animate);
- setTvViewPosition(layoutParams, tvViewFrame, animate);
+ null), layoutParams, animate);
// Update the current display mode.
mTvOptionsManager.onDisplayModeChanged(displayMode);
@@ -814,7 +564,7 @@ public class TvViewUiManager {
return (int) (start + (end - start) * fraction);
}
- private static void interpolateMarginsRelative(MarginLayoutParams out,
+ private static void interpolateMargins(MarginLayoutParams out,
MarginLayoutParams startValue, MarginLayoutParams endValue, float fraction) {
out.topMargin = interpolate(startValue.topMargin, endValue.topMargin, fraction);
out.bottomMargin = interpolate(startValue.bottomMargin, endValue.bottomMargin, fraction);
@@ -825,9 +575,9 @@ public class TvViewUiManager {
out.height = interpolate(startValue.height, endValue.height, fraction);
}
- private MarginLayoutParams createMarginLayoutParams(
+ private FrameLayout.LayoutParams createMarginLayoutParams(
int startMargin, int endMargin, int topMargin, int bottomMargin) {
- MarginLayoutParams lp = new MarginLayoutParams(0, 0);
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(0, 0);
lp.setMarginStart(startMargin);
lp.setMarginEnd(endMargin);
lp.topMargin = topMargin;
@@ -836,4 +586,4 @@ public class TvViewUiManager {
lp.height = mWindowHeight - topMargin - bottomMargin;
return lp;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/ActionItem.java b/src/com/android/tv/ui/sidepanel/ActionItem.java
index 23aff91c..cd70a886 100644
--- a/src/com/android/tv/ui/sidepanel/ActionItem.java
+++ b/src/com/android/tv/ui/sidepanel/ActionItem.java
@@ -17,7 +17,6 @@
package com.android.tv.ui.sidepanel;
import android.view.View;
-import android.widget.ImageView;
import android.widget.TextView;
import com.android.tv.R;
@@ -25,24 +24,14 @@ import com.android.tv.R;
public abstract class ActionItem extends Item {
private final String mTitle;
private final String mDescription;
- private final int mIconId;
public ActionItem(String title) {
- this(title, null, 0);
+ this(title, null);
}
public ActionItem(String title, String description) {
- this(title, description, 0);
- }
-
- public ActionItem(String title, int iconId) {
- this(title, null, iconId);
- }
-
- public ActionItem(String title, String description, int iconId) {
mTitle = title;
mDescription = description;
- mIconId = iconId;
}
@Override
@@ -62,12 +51,5 @@ public abstract class ActionItem extends Item {
} else {
descriptionView.setVisibility(View.GONE);
}
- ImageView iconView = (ImageView) view.findViewById(R.id.icon);
- if (mIconId != 0) {
- iconView.setVisibility(View.VISIBLE);
- iconView.setImageResource(mIconId);
- } else {
- iconView.setVisibility(View.GONE);
- }
}
} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
index d6ccdf6b..341e4350 100644
--- a/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
+++ b/src/com/android/tv/ui/sidepanel/ClosedCaptionFragment.java
@@ -18,6 +18,7 @@ package com.android.tv.ui.sidepanel;
import android.media.tv.TvTrackInfo;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -25,7 +26,6 @@ import android.view.ViewGroup;
import com.android.tv.R;
import com.android.tv.util.CaptionSettings;
-import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -38,8 +38,6 @@ public class ClosedCaptionFragment extends SideFragment {
private String mClosedCaptionLanguage;
private String mClosedCaptionTrackId;
private ClosedCaptionOptionItem mSelectedItem;
- private List<Item> mItems;
- private boolean mPaused;
public ClosedCaptionFragment() {
super(KeyEvent.KEYCODE_CAPTIONS, KeyEvent.KEYCODE_S);
@@ -63,37 +61,32 @@ public class ClosedCaptionFragment extends SideFragment {
mClosedCaptionLanguage = captionSettings.getLanguage();
mClosedCaptionTrackId = captionSettings.getTrackId();
- mItems = new ArrayList<>();
+ List<Item> items = new ArrayList<>();
mSelectedItem = null;
List<TvTrackInfo> tracks = getMainActivity().getTracks(TvTrackInfo.TYPE_SUBTITLE);
if (tracks != null && !tracks.isEmpty()) {
- String trackId = captionSettings.isEnabled() ?
+ String selectedTrackId = captionSettings.isEnabled() ?
getMainActivity().getSelectedTrack(TvTrackInfo.TYPE_SUBTITLE) : null;
- boolean isEnabled = trackId != null;
-
- ClosedCaptionOptionItem item = new ClosedCaptionOptionItem(
- getString(R.string.closed_caption_option_item_off),
- CaptionSettings.OPTION_OFF, null, null);
- // Pick 'Off' as default because we may fail to find the matching language.
- mSelectedItem = item;
- if (!isEnabled) {
+ ClosedCaptionOptionItem item = new ClosedCaptionOptionItem(null, null);
+ items.add(item);
+ if (selectedTrackId == null) {
+ mSelectedItem = item;
item.setChecked(true);
+ setSelectedPosition(0);
}
- mItems.add(item);
-
- for (final TvTrackInfo track : tracks) {
- item = new ClosedCaptionOptionItem(getLabel(track),
- CaptionSettings.OPTION_ON, track.getId(), track.getLanguage());
- if (isEnabled && track.getId().equals(trackId)) {
- item.setChecked(true);
+ for (int i = 0; i < tracks.size(); i++) {
+ item = new ClosedCaptionOptionItem(tracks.get(i), i);
+ if (TextUtils.equals(selectedTrackId, tracks.get(i).getId())) {
mSelectedItem = item;
+ item.setChecked(true);
+ setSelectedPosition(i + 1);
}
- mItems.add(item);
+ items.add(item);
}
}
if (getMainActivity().hasCaptioningSettingsActivity()) {
- mItems.add(new ActionItem(getString(R.string.closed_caption_system_settings),
+ items.add(new ActionItem(getString(R.string.closed_caption_system_settings),
getString(R.string.closed_caption_system_settings_description)) {
@Override
protected void onSelected() {
@@ -103,14 +96,14 @@ public class ClosedCaptionFragment extends SideFragment {
@Override
protected void onFocused() {
super.onFocused();
- if (!mPaused && mSelectedItem != null) {
+ if (mSelectedItem != null) {
getMainActivity().selectSubtitleTrack(
mSelectedItem.mOption, mSelectedItem.mTrackId);
}
}
});
}
- return mItems;
+ return items;
}
@Override
@@ -120,50 +113,6 @@ public class ClosedCaptionFragment extends SideFragment {
}
@Override
- public void onResume() {
- super.onResume();
- if (mPaused) {
- // Apply system's closed caption settings to the UI.
- CaptionSettings captionSettings = getMainActivity().getCaptionSettings();
- mClosedCaptionOption = CaptionSettings.OPTION_SYSTEM;
- mClosedCaptionLanguage = captionSettings.getSystemLanguage();
- ClosedCaptionOptionItem selectedItem = null;
- if (captionSettings.isSystemSettingEnabled()) {
- for (Item item : mItems) {
- if (!(item instanceof ClosedCaptionOptionItem)) {
- continue;
- }
- ClosedCaptionOptionItem captionItem = (ClosedCaptionOptionItem) item;
- if (Utils.isEqualLanguage(captionItem.mLanguage, mClosedCaptionLanguage)) {
- selectedItem = captionItem;
- break;
- }
- }
- }
- if (mSelectedItem != null) {
- mSelectedItem.setChecked(false);
- }
- if (selectedItem == null && mItems.get(0) instanceof ClosedCaptionOptionItem) {
- selectedItem = (ClosedCaptionOptionItem) mItems.get(0);
- }
- if (selectedItem != null) {
- selectedItem.setChecked(true);
- }
- // We shouldn't call MainActivity.selectSubtitleTrack() here because
- // 1. Tracks are not available because video is just started at this moment.
- // 2. MainActivity will apply system settings when video's tracks are available.
- mSelectedItem = selectedItem;
- }
- mPaused = false;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- mPaused = true;
- }
-
- @Override
public void onDestroyView() {
if (mResetClosedCaption) {
getMainActivity().selectSubtitleLanguage(mClosedCaptionOption, mClosedCaptionLanguage,
@@ -172,23 +121,28 @@ public class ClosedCaptionFragment extends SideFragment {
super.onDestroyView();
}
- private String getLabel(TvTrackInfo track) {
- if (track.getLanguage() != null) {
+ private String getLabel(TvTrackInfo track, Integer trackIndex) {
+ if (track == null) {
+ return getString(R.string.closed_caption_option_item_off);
+ } else 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 {
private final int mOption;
private final String mTrackId;
- private final String mLanguage;
- private ClosedCaptionOptionItem(String title, int option, String trackId, String language) {
- super(title);
- mOption = option;
- mTrackId = trackId;
- mLanguage = language;
+ private ClosedCaptionOptionItem(TvTrackInfo track, Integer trackIndex) {
+ super(getLabel(track, trackIndex));
+ if (track == null) {
+ mOption = CaptionSettings.OPTION_OFF;
+ mTrackId = null;
+ } else {
+ mOption = CaptionSettings.OPTION_ON;
+ mTrackId = track.getId();
+ }
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java b/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java
index 7613a9a2..c2746937 100644
--- a/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java
+++ b/src/com/android/tv/ui/sidepanel/CompoundButtonItem.java
@@ -23,9 +23,12 @@ import android.widget.TextView;
import com.android.tv.R;
public abstract class CompoundButtonItem extends Item {
+ private static int sDefaultMaxLine = 0;
+
private final String mCheckedTitle;
private final String mUncheckedTitle;
private final String mDescription;
+ private final int mMaxLine;
private boolean mChecked;
private TextView mTextView;
private CompoundButton mCompoundButton;
@@ -38,6 +41,15 @@ public abstract class CompoundButtonItem extends Item {
mCheckedTitle = checkedTitle;
mUncheckedTitle = uncheckedTitle;
mDescription = description;
+ mMaxLine = 0;
+ }
+
+ public CompoundButtonItem(String checkedTitle, String uncheckedTitle, String description,
+ int maxLine) {
+ mCheckedTitle = checkedTitle;
+ mUncheckedTitle = uncheckedTitle;
+ mDescription = description;
+ mMaxLine = maxLine;
}
protected abstract int getCompoundButtonId();
@@ -57,6 +69,15 @@ public abstract class CompoundButtonItem extends Item {
mTextView = (TextView) view.findViewById(getTitleViewId());
TextView descriptionView = (TextView) view.findViewById(getDescriptionViewId());
if (mDescription != null) {
+ if (mMaxLine != 0) {
+ descriptionView.setMaxLines(mMaxLine);
+ } else {
+ if (sDefaultMaxLine == 0) {
+ sDefaultMaxLine = view.getContext().getResources()
+ .getInteger(R.integer.option_item_description_max_lines);
+ }
+ descriptionView.setMaxLines(sDefaultMaxLine);
+ }
descriptionView.setVisibility(View.VISIBLE);
descriptionView.setText(mDescription);
} else {
diff --git a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
index 9cc54ed2..297e69d9 100644
--- a/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
+++ b/src/com/android/tv/ui/sidepanel/CustomizeChannelListFragment.java
@@ -16,6 +16,8 @@
package com.android.tv.ui.sidepanel;
+import android.content.Context;
+import android.content.SharedPreferences;
import android.media.tv.TvContract.Channels;
import android.os.Bundle;
import android.support.v17.leanback.widget.VerticalGridView;
@@ -27,6 +29,7 @@ import android.widget.TextView;
import com.android.tv.MainActivity;
import com.android.tv.R;
+import com.android.tv.common.SharedPreferencesUtils;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelNumber;
import com.android.tv.ui.OnRepeatedKeyInterceptListener;
@@ -36,39 +39,38 @@ import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
-import java.util.List;
import java.util.Iterator;
+import java.util.List;
public class CustomizeChannelListFragment extends SideFragment {
private static final int GROUP_BY_SOURCE = 0;
private static final int GROUP_BY_HD_SD = 1;
private static final String TRACKER_LABEL = "customize channel list";
- private final List<Channel> mChannels = new ArrayList<>();
- private final long mInitialChannelId;
+ private static final String PREF_KEY_GROUP_SETTINGS = "pref_key_group_settigns";
+ private final List<Channel> mChannels = new ArrayList<>();
+ private long mInitialChannelId = Channel.INVALID_ID;
private long mLastFocusedChannelId = Channel.INVALID_ID;
- private int mGroupingType = GROUP_BY_SOURCE;
+ private static Integer sGroupingType;
private TvInputManagerHelper mInputManager;
private Channel.DefaultComparator mChannelComparator;
private boolean mGroupByFragmentRunning;
private final List<Item> mItems = new ArrayList<>();
- public CustomizeChannelListFragment() {
- this(Channel.INVALID_ID);
- }
-
- public CustomizeChannelListFragment(long initialChannelId) {
- mInitialChannelId = initialChannelId;
- }
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInputManager = getMainActivity().getTvInputManagerHelper();
+ mInitialChannelId = getMainActivity().getCurrentChannelId();
mChannelComparator = new Channel.DefaultComparator(getActivity(), mInputManager);
+ if (sGroupingType == null) {
+ SharedPreferences sharedPreferences = getContext().getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE);
+ sGroupingType = sharedPreferences.getInt(PREF_KEY_GROUP_SETTINGS, GROUP_BY_SOURCE);
+ }
}
@Override
@@ -128,13 +130,16 @@ public class CustomizeChannelListFragment extends SideFragment {
@Override
public void onDestroyView() {
getChannelDataManager().applyUpdatedValuesToDb();
- if (!mGroupByFragmentRunning) {
- getMainActivity().endShrunkenTvView();
- }
super.onDestroyView();
}
@Override
+ public void onDestroy() {
+ super.onDestroy();
+ getMainActivity().endShrunkenTvView();
+ }
+
+ @Override
protected String getTitle() {
return getString(R.string.side_panel_title_edit_channels_for_an_input);
}
@@ -149,7 +154,7 @@ public class CustomizeChannelListFragment extends SideFragment {
mItems.clear();
mChannels.clear();
mChannels.addAll(getChannelDataManager().getChannelList());
- if (mGroupingType == GROUP_BY_SOURCE) {
+ if (sGroupingType == GROUP_BY_SOURCE) {
addItemForGroupBySource(mItems);
} else {
// GROUP_BY_HD_SD
@@ -321,6 +326,49 @@ public class CustomizeChannelListFragment extends SideFragment {
}
}
+ public static class GroupByFragment extends SideFragment {
+ @Override
+ protected String getTitle() {
+ return getString(R.string.side_panel_title_group_by);
+ }
+ @Override
+ public String getTrackerLabel() {
+ return GroupBySubMenu.TRACKER_LABEL;
+ }
+
+ @Override
+ protected List<Item> getItemList() {
+ List<Item> items = new ArrayList<>();
+ items.add(new RadioButtonItem(
+ getString(R.string.edit_channels_group_by_sources)) {
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ setGroupingType(GROUP_BY_SOURCE);
+ closeFragment();
+ }
+ });
+ items.add(new RadioButtonItem(
+ getString(R.string.edit_channels_group_by_hd_sd)) {
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ setGroupingType(GROUP_BY_HD_SD);
+ closeFragment();
+ }
+ });
+ ((RadioButtonItem) items.get(sGroupingType)).setChecked(true);
+ return items;
+ }
+
+ private void setGroupingType(int groupingType) {
+ sGroupingType = groupingType;
+ SharedPreferences sharedPreferences = getContext().getSharedPreferences(
+ SharedPreferencesUtils.SHARED_PREF_UI_SETTINGS, Context.MODE_PRIVATE);
+ sharedPreferences.edit().putInt(PREF_KEY_GROUP_SETTINGS, groupingType).apply();
+ }
+ }
+
private class GroupBySubMenu extends SubMenuItem {
private static final String TRACKER_LABEL = "Group by";
public GroupBySubMenu(String description) {
@@ -330,41 +378,7 @@ public class CustomizeChannelListFragment extends SideFragment {
@Override
protected SideFragment getFragment() {
- return new SideFragment() {
- @Override
- protected String getTitle() {
- return getString(R.string.side_panel_title_group_by);
- }
- @Override
- public String getTrackerLabel() {
- return GroupBySubMenu.TRACKER_LABEL;
- }
-
- @Override
- protected List<Item> getItemList() {
- List<Item> items = new ArrayList<>();
- items.add(new RadioButtonItem(
- getString(R.string.edit_channels_group_by_sources)) {
- @Override
- protected void onSelected() {
- super.onSelected();
- mGroupingType = GROUP_BY_SOURCE;
- closeFragment();
- }
- });
- items.add(new RadioButtonItem(
- getString(R.string.edit_channels_group_by_hd_sd)) {
- @Override
- protected void onSelected() {
- super.onSelected();
- mGroupingType = GROUP_BY_HD_SD;
- closeFragment();
- }
- });
- ((RadioButtonItem) items.get(mGroupingType)).setChecked(true);
- return items;
- }
- };
+ return new GroupByFragment();
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java
index 0d189cca..f633fa5a 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,9 +25,11 @@ 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;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -54,7 +54,15 @@ public class DeveloperOptionFragment extends SideFragment {
@Override
protected List<Item> getItemList() {
List<Item> items = new ArrayList<>();
- if (BuildConfig.ENG) {
+ if (CommonFeatures.DVR.isEnabled(getContext())) {
+ items.add(new ActionItem(getString(R.string.dev_item_dvr_history)) {
+ @Override
+ protected void onSelected() {
+ getMainActivity().getOverlayManager().showDvrHistoryDialog();
+ }
+ });
+ }
+ if (Utils.isDeveloper()) {
items.add(new ActionItem(getString(R.string.dev_item_watch_history)) {
@Override
protected void onSelected() {
@@ -62,18 +70,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)) {
@@ -89,13 +85,18 @@ public class DeveloperOptionFragment extends SideFragment {
TunerPreferences.setStoreTsStream(getContext(), isChecked());
}
});
+ if (Utils.isDeveloper()) {
+ items.add(
+ new ActionItem(getString(R.string.dev_item_show_performance_monitor_log)) {
+ @Override
+ protected void onSelected() {
+ TvApplication.getSingletons(getContext())
+ .getPerformanceMonitor()
+ .startPerformanceMonitorEventDebugActivity(getContext());
+ }
+ });
+ }
return items;
}
-
- /** True if there is the dev options menu */
- public static boolean shouldShow() {
- return Experiments.ENABLE_DEVELOPER_FEATURES.get() || BuildConfig.ENG;
- }
-
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/ui/sidepanel/Item.java b/src/com/android/tv/ui/sidepanel/Item.java
index 00f16427..4e47e75b 100644
--- a/src/com/android/tv/ui/sidepanel/Item.java
+++ b/src/com/android/tv/ui/sidepanel/Item.java
@@ -24,6 +24,7 @@ import android.view.ViewGroup;
public abstract class Item {
private View mItemView;
private boolean mEnabled = true;
+ private boolean mClickable = true;
public void setEnabled(boolean enabled) {
if (mEnabled != enabled) {
@@ -35,6 +36,16 @@ public abstract class Item {
}
/**
+ * Sets the item to be clickable or not.
+ */
+ public void setClickable(boolean clickable) {
+ mClickable = clickable;
+ if (mItemView != null) {
+ mItemView.setClickable(clickable);
+ }
+ }
+
+ /**
* Returns whether this item is enabled.
*/
public boolean isEnabled() {
@@ -64,6 +75,7 @@ public abstract class Item {
*/
protected void onUpdate() {
setEnabledInternal(mItemView, mEnabled);
+ mItemView.setClickable(mClickable);
}
protected abstract void onSelected();
diff --git a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java b/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
deleted file mode 100644
index dec017a8..00000000
--- a/src/com/android/tv/ui/sidepanel/PipInputSelectorFragment.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.ui.sidepanel;
-
-import android.media.tv.TvInputInfo;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.tv.R;
-import com.android.tv.util.PipInputManager;
-import com.android.tv.util.PipInputManager.PipInput;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class PipInputSelectorFragment extends SideFragment {
- private static final String TAG = "PipInputSelector";
- private static final String TRACKER_LABEL = "PIP input source";
-
- private final List<Item> mInputItems = new ArrayList<>();
- private PipInputManager mPipInputManager;
- private PipInput mInitialPipInput;
- private boolean mSelected;
-
- private final PipInputManager.Listener mPipInputListener = new PipInputManager.Listener() {
- @Override
- public void onPipInputStateUpdated() {
- notifyDataSetChanged();
- }
-
- @Override
- public void onPipInputListUpdated() {
- refreshInputList();
- setItems(mInputItems);
- }
- };
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- mPipInputManager = getMainActivity().getPipInputManager();
- mPipInputManager.addListener(mPipInputListener);
- getMainActivity().startShrunkenTvView(false, false);
- return super.onCreateView(inflater, container, savedInstanceState);
- }
-
- @Override
- public void onStart() {
- super.onStart();
- mInitialPipInput = mPipInputManager.getPipInput(getMainActivity().getPipChannel());
- if (mInitialPipInput == null) {
- Log.w(TAG, "PIP should be on");
- closeFragment();
- }
- int count = 0;
- for (Item item : mInputItems) {
- InputItem inputItem = (InputItem) item;
- if (Objects.equals(inputItem.mPipInput, mInitialPipInput)) {
- setSelectedPosition(count);
- break;
- }
- ++count;
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- mPipInputManager.removeListener(mPipInputListener);
- if (!mSelected) {
- getMainActivity().tuneToChannelForPip(mInitialPipInput.getChannel());
- }
- getMainActivity().endShrunkenTvView();
- }
-
- @Override
- protected String getTitle() {
- return getString(R.string.side_panel_title_pip_input_source);
- }
-
- @Override
- public String getTrackerLabel() {
- return TRACKER_LABEL;
- }
-
- @Override
- protected List<Item> getItemList() {
- refreshInputList();
- return mInputItems;
- }
-
- private void refreshInputList() {
- mInputItems.clear();
- for (PipInput input : mPipInputManager.getPipInputList(false)) {
- mInputItems.add(new InputItem(input));
- }
- }
-
- private class InputItem extends RadioButtonItem {
- private final PipInput mPipInput;
-
- private InputItem(PipInput input) {
- super(input.getLongLabel());
- mPipInput = input;
- setEnabled(isAvailable());
- }
-
- @Override
- protected void onUpdate() {
- super.onUpdate();
- setEnabled(mPipInput.isAvailable());
- setChecked(mPipInput == mInitialPipInput);
- }
-
- @Override
- protected void onFocused() {
- super.onFocused();
- if (isEnabled()) {
- getMainActivity().tuneToChannelForPip(mPipInput.getChannel());
- }
- }
-
- @Override
- protected void onSelected() {
- super.onSelected();
- if (isEnabled()) {
- mSelected = true;
- closeFragment();
- }
- }
-
- private boolean isAvailable() {
- if (!mPipInput.isAvailable()) {
- return false;
- }
-
- // If this input shares the same parent with the current main input, you cannot select
- // it. (E.g. two HDMI CEC devices that are connected to HDMI port 1 through an A/V
- // receiver.)
- PipInput pipInput = mPipInputManager.getPipInput(getMainActivity().getCurrentChannel());
- if (pipInput == null) {
- return false;
- }
- TvInputInfo mainInputInfo = pipInput.getInputInfo();
- TvInputInfo pipInputInfo = mPipInput.getInputInfo();
- return mainInputInfo == null || pipInputInfo == null
- || !TextUtils.equals(mainInputInfo.getId(), pipInputInfo.getId())
- && !TextUtils.equals(mainInputInfo.getParentId(), pipInputInfo.getParentId());
- }
- }
-}
diff --git a/src/com/android/tv/ui/sidepanel/SettingsFragment.java b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
index e8033a22..6a5b510c 100644
--- a/src/com/android/tv/ui/sidepanel/SettingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SettingsFragment.java
@@ -16,18 +16,25 @@
package com.android.tv.ui.sidepanel;
+import static com.android.tv.Features.TUNER;
+
+import android.app.ApplicationErrorReport;
+import android.content.Intent;
+import android.media.tv.TvInputInfo;
import android.view.View;
import android.widget.Toast;
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.TvApplication;
+import com.android.tv.customization.TvCustomizationManager;
import com.android.tv.dialog.PinDialogFragment;
-import com.android.tv.dialog.WebDialogFragment;
-import com.android.tv.license.LicenseUtils;
-import com.android.tv.ui.sidepanel.parentalcontrols.ParentalControlsFragment;
+import com.android.tv.license.LicenseSideFragment;
+import com.android.tv.license.Licenses;
+import com.android.tv.tuner.TunerPreferences;
import com.android.tv.util.PermissionUtils;
import com.android.tv.util.SetupUtils;
+import com.android.tv.util.Utils;
import java.util.ArrayList;
import java.util.List;
@@ -38,33 +45,6 @@ import java.util.List;
public class SettingsFragment extends SideFragment {
private static final String TRACKER_LABEL = "settings";
- private final long mCurrentChannelId;
-
- public SettingsFragment(long currentChannelId) {
- mCurrentChannelId = currentChannelId;
- }
-
- /**
- * Opens a dialog showing open source licenses.
- */
- public static final class LicenseActionItem extends ActionItem {
- public final static String DIALOG_TAG = LicenseActionItem.class.getSimpleName();
- public static final String TRACKER_LABEL = "Open Source Licenses";
- private final MainActivity mMainActivity;
-
- public LicenseActionItem(MainActivity mainActivity) {
- super(mainActivity.getString(R.string.settings_menu_licenses));
- mMainActivity = mainActivity;
- }
-
- @Override
- protected void onSelected() {
- WebDialogFragment dialog = WebDialogFragment.newInstance(LicenseUtils.LICENSE_FILE,
- mMainActivity.getString(R.string.dialog_title_licenses), TRACKER_LABEL);
- mMainActivity.getOverlayManager().showDialogFragment(DIALOG_TAG, dialog, false);
- }
- }
-
@Override
protected String getTitle() {
return getResources().getString(R.string.side_panel_title_settings);
@@ -80,11 +60,11 @@ public class SettingsFragment extends SideFragment {
List<Item> items = new ArrayList<>();
final Item customizeChannelListItem = new SubMenuItem(
getString(R.string.settings_channel_source_item_customize_channels),
- getString(R.string.settings_channel_source_item_customize_channels_description), 0,
+ getString(R.string.settings_channel_source_item_customize_channels_description),
getMainActivity().getOverlayManager().getSideFragmentManager()) {
@Override
protected SideFragment getFragment() {
- return new CustomizeChannelListFragment(mCurrentChannelId);
+ return new CustomizeChannelListFragment();
}
@Override
@@ -122,25 +102,11 @@ public class SettingsFragment extends SideFragment {
: R.string.option_toggle_parental_controls_off)) {
@Override
protected void onSelected() {
- final MainActivity tvActivity = getMainActivity();
- final SideFragmentManager sideFragmentManager = tvActivity.getOverlayManager()
- .getSideFragmentManager();
- sideFragmentManager.hideSidePanel(true);
- PinDialogFragment fragment = new PinDialogFragment(
- PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- if (success) {
- sideFragmentManager
- .show(new ParentalControlsFragment(), false);
- sideFragmentManager.showSidePanel(true);
- } else {
- sideFragmentManager.hideAll(false);
- }
- }
- });
- tvActivity.getOverlayManager()
+ getMainActivity().getOverlayManager()
+ .getSideFragmentManager().hideSidePanel(true);
+ PinDialogFragment fragment = PinDialogFragment
+ .create(PinDialogFragment.PIN_DIALOG_TYPE_ENTER_PIN);
+ getMainActivity().getOverlayManager()
.showDialogFragment(PinDialogFragment.DIALOG_TAG, fragment, true);
}
});
@@ -149,12 +115,73 @@ public class SettingsFragment extends SideFragment {
// But, we may be able to turn on channel lock feature regardless of the permission.
// It's TBD.
}
- if (LicenseUtils.hasLicenses(activity.getAssets())) {
- items.add(new LicenseActionItem(activity));
+ boolean showTrickplaySetting = false;
+ if (TUNER.isEnabled(getContext())) {
+ for (TvInputInfo inputInfo : TvApplication.getSingletons(getContext())
+ .getTvInputManagerHelper().getTvInputInfos(true, true)) {
+ if (Utils.isInternalTvInput(getContext(), inputInfo.getId())) {
+ showTrickplaySetting = true;
+ break;
+ }
+ }
+ if (showTrickplaySetting) {
+ showTrickplaySetting =
+ TvCustomizationManager.getTrickplayMode(getContext())
+ == TvCustomizationManager.TRICKPLAY_MODE_ENABLED;
+ }
+ }
+ if (showTrickplaySetting) {
+ items.add(
+ new SwitchItem(getString(R.string.settings_trickplay),
+ getString(R.string.settings_trickplay),
+ getString(R.string.settings_trickplay_description),
+ getResources().getInteger(R.integer.trickplay_description_max_lines)) {
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ boolean enabled = TunerPreferences.getTrickplaySetting(getContext())
+ != TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ setChecked(enabled);
+ }
+
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ @TunerPreferences.TrickplaySetting int setting =
+ isChecked() ? TunerPreferences.TRICKPLAY_SETTING_ENABLED
+ : TunerPreferences.TRICKPLAY_SETTING_DISABLED;
+ TunerPreferences.setTrickplaySetting(getContext(), setting);
+ }
+ });
+ }
+ 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 (Licenses.hasLicenses(getContext())) {
+ items.add(
+ new SubMenuItem(
+ getString(R.string.settings_menu_licenses),
+ getMainActivity().getOverlayManager().getSideFragmentManager()) {
+ @Override
+ protected SideFragment getFragment() {
+ return new LicenseSideFragment();
+ }
+ });
}
// 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..6bd921a2 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragment.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragment.java
@@ -26,32 +26,36 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.FrameLayout;
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 abstract class SideFragment<T extends Item> 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_IDS = {
+ private static final int PRELOAD_VIEW_SIZE = 7;
+ private static final int[] PRELOAD_VIEW_IDS = {
R.layout.option_item_radio_button,
R.layout.option_item_channel_lock,
R.layout.option_item_check_box,
- R.layout.option_item_channel_check
+ R.layout.option_item_channel_check,
+ R.layout.option_item_action
};
- private static RecyclerView.RecycledViewPool sRecycledViewPool;
+ private static RecyclerView.RecycledViewPool sRecycledViewPool =
+ new RecyclerView.RecycledViewPool();
private VerticalGridView mListView;
private ItemAdapter mAdapter;
@@ -89,14 +93,8 @@ 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);
+ View view = ViewCache.getInstance().getOrCreateView(
+ inflater, getFragmentLayoutResourceId(), container);
TextView textView = (TextView) view.findViewById(R.id.side_panel_title);
textView.setText(getTitle());
@@ -158,7 +156,7 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
return mListView.getSelectedPosition();
}
- public void setItems(List<Item> items) {
+ public void setItems(List<T> items) {
mAdapter.reset(items);
}
@@ -229,56 +227,50 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
protected abstract String getTitle();
@Override
public abstract String getTrackerLabel();
- protected abstract List<Item> getItemList();
+ protected abstract List<T> getItemList();
public interface SideFragmentListener {
void onSideFragmentViewDestroyed();
}
/**
- * Preloads the view holders.
+ * Preloads the item views.
*/
- public static void preloadRecycledViews(Context context) {
- if (sRecycledViewPool != null) {
- return;
- }
- sRecycledViewPool = new RecyclerView.RecycledViewPool();
- LayoutInflater inflater =
- (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- 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);
- }
+ public static void preloadItemViews(Context context) {
+ ViewCache.getInstance().putView(
+ context, R.layout.option_fragment, new FrameLayout(context), 1);
+ VerticalGridView fakeParent = new VerticalGridView(context);
+ for (int id : PRELOAD_VIEW_IDS) {
+ sRecycledViewPool.setMaxRecycledViews(id, PRELOAD_VIEW_SIZE);
+ ViewCache.getInstance().putView(context, id, fakeParent, PRELOAD_VIEW_SIZE);
}
}
/**
- * Releases the pre-loaded view holders.
+ * Releases the recycled view pool.
*/
- public static void releasePreloadedRecycledViews() {
- sRecycledViewPool = null;
+ public static void releaseRecycledViewPool() {
+ sRecycledViewPool.clear();
}
- private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {
+ private static class ItemAdapter<T extends Item> extends RecyclerView.Adapter<ViewHolder> {
private final LayoutInflater mLayoutInflater;
- private List<Item> mItems;
+ private List<T> mItems;
- private ItemAdapter(LayoutInflater layoutInflater, List<Item> items) {
+ private ItemAdapter(LayoutInflater layoutInflater, List<T> items) {
mLayoutInflater = layoutInflater;
mItems = items;
}
- private void reset(List<Item> items) {
+ private void reset(List<T> items) {
mItems = items;
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false));
+ View view = ViewCache.getInstance().getOrCreateView(mLayoutInflater, viewType, parent);
+ return new ViewHolder(view);
}
@Override
@@ -301,11 +293,11 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
return mItems == null ? 0 : mItems.size();
}
- private Item getItem(int position) {
+ private T getItem(int position) {
return mItems.get(position);
}
- private void clearRadioGroup(Item item) {
+ private void clearRadioGroup(T item) {
int position = mItems.indexOf(item);
for (int i = position - 1; i >= 0; --i) {
if ((item = mItems.get(i)) instanceof RadioButtonItem) {
@@ -322,55 +314,57 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel {
}
}
}
+ }
- private static class ViewHolder extends RecyclerView.ViewHolder
- implements View.OnClickListener, View.OnFocusChangeListener {
- private ItemAdapter mAdapter;
- public Item mItem;
+ private static class ViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener, View.OnFocusChangeListener {
+ private ItemAdapter mAdapter;
+ public Item mItem;
- private ViewHolder(View view) {
- super(view);
- itemView.setOnClickListener(this);
- itemView.setOnFocusChangeListener(this);
- }
+ private ViewHolder(View view) {
+ super(view);
+ itemView.setOnClickListener(this);
+ itemView.setOnFocusChangeListener(this);
+ }
- public void onBind(ItemAdapter adapter, Item item) {
- mAdapter = adapter;
- mItem = item;
- mItem.onBind(itemView);
- mItem.onUpdate();
- }
+ public void onBind(ItemAdapter adapter, Item item) {
+ mAdapter = adapter;
+ mItem = item;
+ mItem.onBind(itemView);
+ mItem.onUpdate();
+ }
- public void onUnbind() {
- mItem.onUnbind();
- mItem = null;
- mAdapter = null;
- }
+ public void onUnbind() {
+ mItem.onUnbind();
+ mItem = null;
+ mAdapter = null;
+ }
- @Override
- public void onClick(View view) {
- if (mItem instanceof RadioButtonItem) {
- mAdapter.clearRadioGroup(mItem);
- }
- if (view.getBackground() instanceof RippleDrawable) {
- view.postDelayed(new Runnable() {
- @Override
- public void run() {
- if (mItem != null) {
- mItem.onSelected();
+ @Override
+ public void onClick(View view) {
+ if (mItem instanceof RadioButtonItem) {
+ mAdapter.clearRadioGroup(mItem);
+ }
+ if (view.getBackground() instanceof RippleDrawable) {
+ view.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mItem != null) {
+ mItem.onSelected();
+ }
}
- }
- }, view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
- } else {
- mItem.onSelected();
- }
+ },
+ view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
+ } else {
+ mItem.onSelected();
}
+ }
- @Override
- public void onFocusChange(View view, boolean focusGained) {
- if (focusGained) {
- mItem.onFocused();
- }
+ @Override
+ public void onFocusChange(View view, boolean focusGained) {
+ if (focusGained) {
+ mItem.onFocused();
}
}
}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
index 553cd9d7..d02d3fb7 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
@@ -24,6 +24,7 @@ import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Handler;
import android.view.View;
+import android.view.ViewTreeObserver;
import com.android.tv.R;
@@ -34,6 +35,7 @@ public class SideFragmentManager {
private final FragmentManager mFragmentManager;
private final Runnable mPreShowRunnable;
private final Runnable mPostHideRunnable;
+ private ViewTreeObserver.OnGlobalLayoutListener mShowOnGlobalLayoutListener;
// To get the count reliably while using popBackStack(),
// instead of using getBackStackEntryCount() with popBackStackImmediate().
@@ -99,17 +101,10 @@ public class SideFragmentManager {
* Shows the given {@link SideFragment}.
*/
public void show(SideFragment sideFragment, boolean showEnterAnimation) {
- SideFragment.preloadRecycledViews(mActivity);
if (isHiding()) {
mHideAnimator.end();
}
boolean isFirst = (mFragmentCount == 0);
- if (isFirst) {
- if (mPreShowRunnable != null) {
- mPreShowRunnable.run();
- }
- }
-
FragmentTransaction ft = mFragmentManager.beginTransaction();
if (!isFirst) {
ft.setCustomAnimations(
@@ -123,8 +118,22 @@ public class SideFragmentManager {
mFragmentCount++;
if (isFirst) {
+ // We should wait for fragment transition and intital layouting finished to start the
+ // slide-in animation to prevent jankiness resulted by performing transition and
+ // layouting at the same time with animation.
mPanel.setVisibility(View.VISIBLE);
- mShowAnimator.start();
+ mShowOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ mShowOnGlobalLayoutListener = null;
+ if (mPreShowRunnable != null) {
+ mPreShowRunnable.run();
+ }
+ mShowAnimator.start();
+ }
+ };
+ mPanel.getViewTreeObserver().addOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
}
scheduleHideAll();
}
@@ -142,6 +151,18 @@ public class SideFragmentManager {
}
public void hideAll(boolean withAnimation) {
+ if (mShowAnimator.isStarted()) {
+ mShowAnimator.end();
+ }
+ if (mShowOnGlobalLayoutListener != null) {
+ // The show operation maybe requested but the show animator is not started yet, in this
+ // case, we show still run mPreShowRunnable.
+ mPanel.getViewTreeObserver().removeOnGlobalLayoutListener(mShowOnGlobalLayoutListener);
+ mShowOnGlobalLayoutListener = null;
+ if (mPreShowRunnable != null) {
+ mPreShowRunnable.run();
+ }
+ }
if (withAnimation) {
if (!isHiding()) {
mHideAnimator.start();
@@ -178,7 +199,6 @@ public class SideFragmentManager {
* @param withAnimation specifies if animation should be shown.
*/
public void showSidePanel(boolean withAnimation) {
- SideFragment.preloadRecycledViews(mActivity);
if (mFragmentCount == 0) {
return;
}
diff --git a/src/com/android/tv/ui/sidepanel/SimpleItem.java b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java
index 52a5f13f..42553b66 100644
--- a/src/com/android/tv/ui/sidepanel/SimpleItem.java
+++ b/src/com/android/tv/ui/sidepanel/SimpleActionItem.java
@@ -19,12 +19,12 @@ package com.android.tv.ui.sidepanel;
/**
* A simple item which shows title and description.
*/
-public class SimpleItem extends ActionItem {
- public SimpleItem(String title) {
+public class SimpleActionItem extends ActionItem {
+ public SimpleActionItem(String title) {
super(title);
}
- public SimpleItem(String title, String description) {
+ public SimpleActionItem(String title, String description) {
super(title, description);
}
diff --git a/src/com/android/tv/ui/sidepanel/SubMenuItem.java b/src/com/android/tv/ui/sidepanel/SubMenuItem.java
index aa349dbd..4b0e8e2c 100644
--- a/src/com/android/tv/ui/sidepanel/SubMenuItem.java
+++ b/src/com/android/tv/ui/sidepanel/SubMenuItem.java
@@ -21,20 +21,11 @@ public abstract class SubMenuItem extends ActionItem {
private final SideFragmentManager mSideFragmentManager;
public SubMenuItem(String title, SideFragmentManager fragmentManager) {
- this(title, null, 0, fragmentManager);
+ this(title, null, fragmentManager);
}
public SubMenuItem(String title, String description, SideFragmentManager fragmentManager) {
- this(title, description, 0, fragmentManager);
- }
-
- public SubMenuItem(String title, int iconId, SideFragmentManager fragmentManager) {
- this(title, null, iconId, fragmentManager);
- }
-
- public SubMenuItem(String title, String description, int iconId,
- SideFragmentManager fragmentManager) {
- super(title, description, iconId);
+ super(title, description);
mSideFragmentManager = fragmentManager;
}
diff --git a/src/com/android/tv/ui/sidepanel/SwitchItem.java b/src/com/android/tv/ui/sidepanel/SwitchItem.java
index ef9966a5..06591b62 100644
--- a/src/com/android/tv/ui/sidepanel/SwitchItem.java
+++ b/src/com/android/tv/ui/sidepanel/SwitchItem.java
@@ -31,6 +31,11 @@ public class SwitchItem extends CompoundButtonItem {
super(checkedTitle, uncheckedTitle, description);
}
+ public SwitchItem(String checkedTitle, String uncheckedTitle, String description,
+ int maxLines) {
+ super(checkedTitle, uncheckedTitle, description, maxLines);
+ }
+
@Override
protected int getResourceId() {
return R.layout.option_item_switch;
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java
index da712924..9a4879fc 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/ParentalControlsFragment.java
@@ -130,17 +130,8 @@ public class ParentalControlsFragment extends SideFragment {
protected void onSelected() {
final MainActivity tvActivity = getMainActivity();
tvActivity.getOverlayManager().getSideFragmentManager().hideSidePanel(true);
-
- PinDialogFragment fragment =
- new PinDialogFragment(
- PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN,
- new PinDialogFragment.ResultListener() {
- @Override
- public void done(boolean success) {
- tvActivity.getOverlayManager().getSideFragmentManager()
- .showSidePanel(true);
- }
- });
+ PinDialogFragment fragment = PinDialogFragment.create(
+ PinDialogFragment.PIN_DIALOG_TYPE_NEW_PIN);
tvActivity.getOverlayManager().showDialogFragment(PinDialogFragment.DIALOG_TAG,
fragment, true);
}
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
index 6bc47939..7c8cecbe 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/RatingsFragment.java
@@ -17,16 +17,17 @@
package com.android.tv.ui.sidepanel.parentalcontrols;
import android.graphics.drawable.Drawable;
+import android.media.tv.TvContentRating;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SparseIntArray;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
-
import com.android.tv.MainActivity;
import com.android.tv.R;
import com.android.tv.dialog.WebDialogFragment;
+import com.android.tv.experiments.Experiments;
import com.android.tv.license.LicenseUtils;
import com.android.tv.parental.ContentRatingSystem;
import com.android.tv.parental.ContentRatingSystem.Rating;
@@ -38,7 +39,6 @@ import com.android.tv.ui.sidepanel.RadioButtonItem;
import com.android.tv.ui.sidepanel.SideFragment;
import com.android.tv.util.TvSettings;
import com.android.tv.util.TvSettings.ContentRatingLevel;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -77,6 +77,7 @@ public class RatingsFragment extends SideFragment {
private final List<RatingLevelItem> mRatingLevelItems = new ArrayList<>();
// A map from the rating system ID string to RatingItem objects.
private final Map<String, List<RatingItem>> mContentRatingSystemItemMap = new ArrayMap<>();
+ private CheckBoxItem mBlockUnratedItem;
private ParentalControlSettings mParentalControlSettings;
public static String getDescription(MainActivity tvActivity) {
@@ -102,6 +103,12 @@ public class RatingsFragment extends SideFragment {
protected List<Item> getItemList() {
List<Item> items = new ArrayList<>();
+ if (mBlockUnratedItem != null
+ && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+ items.add(mBlockUnratedItem);
+ items.add(new DividerItem());
+ }
+
mRatingLevelItems.clear();
for (int i = 0; i < sLevelResourceIdMap.size(); ++i) {
mRatingLevelItems.add(new RatingLevelItem(sLevelResourceIdMap.keyAt(i)));
@@ -152,6 +159,28 @@ public class RatingsFragment extends SideFragment {
super.onCreate(savedInstanceState);
mParentalControlSettings = getMainActivity().getParentalControlSettings();
mParentalControlSettings.loadRatings();
+ if (Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+ mBlockUnratedItem =
+ new CheckBoxItem(
+ getResources().getString(R.string.option_block_unrated_programs)) {
+
+ @Override
+ protected void onUpdate() {
+ super.onUpdate();
+ setChecked(
+ mParentalControlSettings.isRatingBlocked(
+ new TvContentRating[] {TvContentRating.UNRATED}));
+ }
+
+ @Override
+ protected void onSelected() {
+ super.onSelected();
+ if (mParentalControlSettings.setUnratedBlocked(isChecked())) {
+ updateRatingLevels();
+ }
+ }
+ };
+ }
}
@Override
@@ -202,6 +231,13 @@ public class RatingsFragment extends SideFragment {
super.onSelected();
mParentalControlSettings.setContentRatingLevel(
getMainActivity().getContentRatingsManager(), mRatingLevel);
+ if (mBlockUnratedItem != null
+ && Boolean.TRUE.equals(Experiments.ENABLE_UNRATED_CONTENT_SETTINGS.get())) {
+ // set checked if UNRATED is blocked, and set unchecked otherwise.
+ mBlockUnratedItem.setChecked(
+ mParentalControlSettings.isRatingBlocked(
+ new TvContentRating[] {TvContentRating.UNRATED}));
+ }
notifyItemsChanged(mRatingLevelItems.size());
}
}
@@ -302,7 +338,7 @@ public class RatingsFragment extends SideFragment {
@Override
protected void onSelected() {
getMainActivity().getOverlayManager().getSideFragmentManager()
- .show(new SubRatingsFragment(mContentRatingSystem, mRating));
+ .show(SubRatingsFragment.create(mContentRatingSystem, mRating.getName()));
}
@Override
diff --git a/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java b/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java
index f6612fdb..4634b74c 100644
--- a/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java
+++ b/src/com/android/tv/ui/sidepanel/parentalcontrols/SubRatingsFragment.java
@@ -17,6 +17,7 @@
package com.android.tv.ui.sidepanel.parentalcontrols;
import android.graphics.drawable.Drawable;
+import android.os.Bundle;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
@@ -36,13 +37,34 @@ import java.util.List;
public class SubRatingsFragment extends SideFragment {
private static final String TRACKER_LABEL = "Sub ratings";
- private final ContentRatingSystem mContentRatingSystem;
- private final Rating mRating;
+ private static final String ARGS_CONTENT_RATING_SYSTEM_ID = "args_content_rating_system_id";
+ private static final String ARGS_RATING_NAME = "args_rating_name";
+
+ private ContentRatingSystem mContentRatingSystem;
+ private Rating mRating;
private final List<SubRatingItem> mSubRatingItems = new ArrayList<>();
- public SubRatingsFragment(ContentRatingSystem contentRatingSystem, Rating rating) {
- mContentRatingSystem = contentRatingSystem;
- mRating = rating;
+ public static SubRatingsFragment create(ContentRatingSystem contentRatingSystem,
+ String ratingName) {
+ SubRatingsFragment fragment = new SubRatingsFragment();
+ Bundle args = new Bundle();
+ args.putString(ARGS_CONTENT_RATING_SYSTEM_ID, contentRatingSystem.getId());
+ args.putString(ARGS_RATING_NAME, ratingName);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mContentRatingSystem = getMainActivity().getContentRatingsManager()
+ .getContentRatingSystem(getArguments().getString(ARGS_CONTENT_RATING_SYSTEM_ID));
+ if (mContentRatingSystem != null) {
+ mRating = mContentRatingSystem.getRating(getArguments().getString(ARGS_RATING_NAME));
+ }
+ if (mRating == null) {
+ closeFragment();
+ }
}
@Override
diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java
index 78243642..477412e4 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;
@@ -76,7 +76,7 @@ public abstract class AsyncDbTask<Params, Progress, Result>
* accepted for execution
* @throws NullPointerException if command is null
*/
- public static void execute(Runnable command) {
+ public static void executeOnDbThread(Runnable command) {
DB_EXECUTOR.execute(command);
}
diff --git a/src/com/android/tv/util/BitmapUtils.java b/src/com/android/tv/util/BitmapUtils.java
index d45a8dce..fbaab023 100644
--- a/src/com/android/tv/util/BitmapUtils.java
+++ b/src/com/android/tv/util/BitmapUtils.java
@@ -24,6 +24,7 @@ import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.net.TrafficStats;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
@@ -56,6 +57,12 @@ public final class BitmapUtils {
return Bitmap.createScaledBitmap(bm, rect.right, rect.bottom, false);
}
+ public static Bitmap getScaledMutableBitmap(Bitmap bm, int maxWidth, int maxHeight) {
+ Bitmap scaledBitmap = scaleBitmap(bm, maxWidth, maxHeight);
+ return scaledBitmap.isMutable() ? scaledBitmap
+ : scaledBitmap.copy(Bitmap.Config.ARGB_8888, true);
+ }
+
private static Rect calculateNewSize(Bitmap bm, int maxWidth, int maxHeight) {
final double ratio = maxHeight / (double) maxWidth;
final double bmRatio = bm.getHeight() / (double) bm.getWidth();
@@ -89,6 +96,8 @@ public final class BitmapUtils {
boolean isResourceUri = isContentResolverUri(uri);
URLConnection urlConnection = null;
InputStream inputStream = null;
+ final int oldTag = TrafficStats.getThreadStatsTag();
+ TrafficStats.setThreadStatsTag(NetworkTrafficTags.LOGO_FETCHER);
try {
if (isResourceUri) {
inputStream = context.getContentResolver().openInputStream(uri);
@@ -142,6 +151,7 @@ public final class BitmapUtils {
return null;
} finally {
close(inputStream, urlConnection);
+ TrafficStats.setThreadStatsTag(oldTag);
}
}
diff --git a/src/com/android/tv/util/Debug.java b/src/com/android/tv/util/Debug.java
new file mode 100644
index 00000000..67a2683d
--- /dev/null
+++ b/src/com/android/tv/util/Debug.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class only for help developers.
+ */
+public class Debug {
+ /**
+ * A threshold of start up time, when the start up time of Live TV is more than it,
+ * a warning will show to the developer.
+ */
+ public static final long TIME_START_UP_DURATION_THRESHOLD = TimeUnit.SECONDS.toMillis(6);
+ /**
+ * Tag for measuring start up time of Live TV.
+ */
+ public static final String TAG_START_UP_TIMER = "start_up_timer";
+
+ /**
+ * A global map for duration timers.
+ */
+ private final static Map<String, DurationTimer> sTimerMap = new HashMap<>();
+
+ /**
+ * Returns the global duration timer by tag.
+ */
+ public static DurationTimer getTimer(String tag) {
+ if (sTimerMap.get(tag) != null) {
+ return sTimerMap.get(tag);
+ }
+ DurationTimer timer = new DurationTimer(tag, true);
+ sTimerMap.put(tag, timer);
+ return timer;
+ }
+
+ /**
+ * Removes the global duration timer by tag.
+ */
+ public static DurationTimer removeTimer(String tag) {
+ return sTimerMap.remove(tag);
+ }
+}
diff --git a/src/com/android/tv/analytics/DurationTimer.java b/src/com/android/tv/util/DurationTimer.java
index ad2d91f8..1f057bf6 100644
--- a/src/com/android/tv/analytics/DurationTimer.java
+++ b/src/com/android/tv/util/DurationTimer.java
@@ -14,30 +14,50 @@
* limitations under the License.
*/
-package com.android.tv.analytics;
+package com.android.tv.util;
import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.tv.common.BuildConfig;
/**
* Times a duration.
*/
public final class DurationTimer {
+ private static final String TAG = "DurationTimer";
public static final long TIME_NOT_SET = -1;
- private long startTimeMs = TIME_NOT_SET;
+ private long mStartTimeMs = 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;
+ return mStartTimeMs != TIME_NOT_SET;
}
/**
* Start the timer.
*/
public void start() {
- startTimeMs = SystemClock.elapsedRealtime();
+ mStartTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Returns true if timer is started.
+ */
+ public boolean isStarted() {
+ return mStartTimeMs != TIME_NOT_SET;
}
/**
@@ -45,7 +65,7 @@ public final class DurationTimer {
* running.
*/
public long getDuration() {
- return isRunning() ? SystemClock.elapsedRealtime() - startTimeMs : TIME_NOT_SET;
+ return isRunning() ? SystemClock.elapsedRealtime() - mStartTimeMs : TIME_NOT_SET;
}
/**
@@ -56,7 +76,16 @@ public final class DurationTimer {
*/
public long reset() {
long duration = getDuration();
- startTimeMs = TIME_NOT_SET;
+ mStartTimeMs = 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/ImageLoader.java b/src/com/android/tv/util/ImageLoader.java
index 04bb478a..86bb94c1 100644
--- a/src/com/android/tv/util/ImageLoader.java
+++ b/src/com/android/tv/util/ImageLoader.java
@@ -292,7 +292,8 @@ public final class ImageLoader {
* Checks if a reload would be needed if the results of other was available.
*/
private boolean isReloadNeeded(LoadBitmapTask other) {
- return mMaxHeight >= other.mMaxHeight * 2 || mMaxWidth >= other.mMaxWidth * 2;
+ return (other.mMaxHeight != Integer.MAX_VALUE && mMaxHeight >= other.mMaxHeight * 2)
+ || (other.mMaxWidth != Integer.MAX_VALUE && mMaxWidth >= other.mMaxWidth * 2);
}
@Nullable
diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java
index 8e3b59e9..d5d7bee3 100644
--- a/src/com/android/tv/util/LocationUtils.java
+++ b/src/com/android/tv/util/LocationUtils.java
@@ -16,15 +16,20 @@
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;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.util.Log;
+import com.android.tv.tuner.util.PostalCodeUtils;
import java.io.IOException;
import java.util.List;
@@ -39,6 +44,7 @@ public class LocationUtils {
private static Context sApplicationContext;
private static Address sAddress;
+ private static String sCountry;
private static IOException sError;
/**
@@ -59,6 +65,18 @@ public class LocationUtils {
return null;
}
+ /** Returns the current country. */
+ @NonNull
+ public static synchronized String getCurrentCountry(Context context) {
+ if (sCountry != null) {
+ return sCountry;
+ }
+ if (TextUtils.isEmpty(sCountry)) {
+ 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 +86,14 @@ public class LocationUtils {
try {
List<Address> addresses = geocoder.getFromLocation(
location.getLatitude(), location.getLongitude(), 1);
- if (addresses != null) {
+ if (addresses != null && !addresses.isEmpty()) {
sAddress = addresses.get(0);
if (DEBUG) Log.d(TAG, "Got " + sAddress);
+ try {
+ PostalCodeUtils.updatePostalCode(sApplicationContext);
+ } catch (Exception e) {
+ // Do nothing
+ }
} else {
if (DEBUG) Log.d(TAG, "No address returned");
}
diff --git a/src/com/android/tv/util/NetworkTrafficTags.java b/src/com/android/tv/util/NetworkTrafficTags.java
new file mode 100644
index 00000000..2dca613c
--- /dev/null
+++ b/src/com/android/tv/util/NetworkTrafficTags.java
@@ -0,0 +1,64 @@
+/*
+ * 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.net.TrafficStats;
+import android.support.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+
+/** Constants for tagging network traffic in the Live channels app. */
+public final class NetworkTrafficTags {
+
+ public static final int DEFAULT_LIVE_CHANNELS = 1;
+ public static final int LOGO_FETCHER = 2;
+ public static final int HDHOMERUN = 3;
+ public static final int EPG_FETCH = 4;
+
+ /**
+ * An executor which simply wraps a provided delegate executor, but calls {@link
+ * TrafficStats#setThreadStatsTag(int)} before executing any task.
+ */
+ public static class TrafficStatsTaggingExecutor implements Executor {
+ private final Executor delegateExecutor;
+ private final int tag;
+
+ public TrafficStatsTaggingExecutor(Executor delegateExecutor, int tag) {
+ this.delegateExecutor = delegateExecutor;
+ this.tag = tag;
+ }
+
+ @Override
+ public void execute(final @NonNull Runnable command) {
+ // TODO(b/62038127): robolectric does not support lamdas in unbundled apps
+ delegateExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ TrafficStats.setThreadStatsTag(tag);
+ try {
+ command.run();
+ } finally {
+ TrafficStats.clearThreadStatsTag();
+ }
+ }
+ });
+ }
+ }
+
+ private NetworkTrafficTags() {}
+}
diff --git a/src/com/android/tv/util/OnboardingUtils.java b/src/com/android/tv/util/OnboardingUtils.java
index 3040020e..49b02b82 100644
--- a/src/com/android/tv/util/OnboardingUtils.java
+++ b/src/com/android/tv/util/OnboardingUtils.java
@@ -16,17 +16,10 @@
package com.android.tv.util;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.database.Cursor;
-import android.media.tv.TvContract.Channels;
import android.net.Uri;
import android.preference.PreferenceManager;
-import android.support.annotation.UiThread;
-
-import com.android.tv.TvApplication;
-import com.android.tv.data.ChannelDataManager;
/**
* A utility class related to onboarding experience.
@@ -82,41 +75,11 @@ public final class OnboardingUtils {
}
/**
- * Checks whether the onboarding screen should be shown or not.
- */
- public static boolean needToShowOnboarding(Context context) {
- return isFirstRunWithCurrentVersion(context) || !areChannelsAvailable(context);
- }
-
- /**
- * Checks if there are any available tuner channels.
- */
- @UiThread
- public static boolean areChannelsAvailable(Context context) {
- ChannelDataManager manager = TvApplication.getSingletons(context).getChannelDataManager();
- if (manager.isDbLoadFinished()) {
- return manager.getChannelCount() != 0;
- }
- // This method should block the UI thread.
- ContentResolver resolver = context.getContentResolver();
- try (Cursor c = resolver.query(Channels.CONTENT_URI, new String[] {Channels._ID}, null,
- null, null)) {
- return c != null && c.getCount() != 0;
- }
- }
-
- /**
- * Checks if there are any available TV inputs.
- */
- public static boolean areInputsAvailable(Context context) {
- return TvApplication.getSingletons(context).getTvInputManagerHelper()
- .getTvInputInfos(true, false).size() > 0;
- }
-
- /**
* Returns merchant collection URL.
*/
private static String getMerchantCollectionUrl() {
return "TODO: add a merchant collection url";
}
+
+ private OnboardingUtils() {}
}
diff --git a/src/com/android/tv/util/Partner.java b/src/com/android/tv/util/Partner.java
new file mode 100644
index 00000000..e3688392
--- /dev/null
+++ b/src/com/android/tv/util/Partner.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.media.tv.TvInputInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This file refers to Partner.java in LeanbackLauncher. Interact with partner customizations. There
+ * can only be one set of customizations on a device, and it must be bundled with the system.
+ */
+public class Partner {
+ private static final String TAG = "Partner";
+ /** Marker action used to discover partner */
+ private static final String ACTION_PARTNER_CUSTOMIZATION =
+ "com.google.android.leanbacklauncher.action.PARTNER_CUSTOMIZATION";
+
+ /** ID tags for device input types */
+ public static final String INPUT_TYPE_BUNDLED_TUNER = "input_type_combined_tuners";
+ public static final String INPUT_TYPE_TUNER = "input_type_tuner";
+ public static final String INPUT_TYPE_CEC_LOGICAL = "input_type_cec_logical";
+ public static final String INPUT_TYPE_CEC_RECORDER = "input_type_cec_recorder";
+ public static final String INPUT_TYPE_CEC_PLAYBACK = "input_type_cec_playback";
+ public static final String INPUT_TYPE_MHL_MOBILE = "input_type_mhl_mobile";
+ public static final String INPUT_TYPE_HDMI = "input_type_hdmi";
+ public static final String INPUT_TYPE_DVI = "input_type_dvi";
+ public static final String INPUT_TYPE_COMPONENT = "input_type_component";
+ public static final String INPUT_TYPE_SVIDEO = "input_type_svideo";
+ public static final String INPUT_TYPE_COMPOSITE = "input_type_composite";
+ public static final String INPUT_TYPE_DISPLAY_PORT = "input_type_displayport";
+ public static final String INPUT_TYPE_VGA = "input_type_vga";
+ public static final String INPUT_TYPE_SCART = "input_type_scart";
+ public static final String INPUT_TYPE_OTHER = "input_type_other";
+
+ private static final String INPUTS_ORDER = "home_screen_inputs_ordering";
+ private static final String TYPE_ARRAY = "array";
+
+ private static Partner sPartner;
+ private static final Object sLock = new Object();
+
+ private final String mPackageName;
+ private final String mReceiverName;
+ private final Resources mResources;
+
+ private static final Map<String, Integer> INPUT_TYPE_MAP = new HashMap<>();
+ static {
+ INPUT_TYPE_MAP.put(INPUT_TYPE_BUNDLED_TUNER, TvInputManagerHelper.TYPE_BUNDLED_TUNER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_TUNER, TvInputInfo.TYPE_TUNER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_LOGICAL, TvInputManagerHelper.TYPE_CEC_DEVICE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_RECORDER, TvInputManagerHelper.TYPE_CEC_DEVICE_RECORDER);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_CEC_PLAYBACK, TvInputManagerHelper.TYPE_CEC_DEVICE_PLAYBACK);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_MHL_MOBILE, TvInputManagerHelper.TYPE_MHL_MOBILE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_HDMI, TvInputInfo.TYPE_HDMI);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_DVI, TvInputInfo.TYPE_DVI);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_COMPONENT, TvInputInfo.TYPE_COMPONENT);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_SVIDEO, TvInputInfo.TYPE_SVIDEO);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_COMPOSITE, TvInputInfo.TYPE_COMPOSITE);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_DISPLAY_PORT, TvInputInfo.TYPE_DISPLAY_PORT);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_VGA, TvInputInfo.TYPE_VGA);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_SCART, TvInputInfo.TYPE_SCART);
+ INPUT_TYPE_MAP.put(INPUT_TYPE_OTHER, TvInputInfo.TYPE_OTHER);
+ }
+
+ private Partner(String packageName, String receiverName, Resources res) {
+ mPackageName = packageName;
+ mReceiverName = receiverName;
+ mResources = res;
+ }
+
+ /** Returns partner instance. */
+ public static Partner getInstance(Context context) {
+ PackageManager pm = context.getPackageManager();
+ synchronized (sLock) {
+ ResolveInfo info = getPartnerResolveInfo(pm);
+ if (info != null) {
+ final String packageName = info.activityInfo.packageName;
+ final String receiverName = info.activityInfo.name;
+ try {
+ final Resources res = pm.getResourcesForApplication(packageName);
+ sPartner = new Partner(packageName, receiverName, res);
+ sPartner.sendInitBroadcast(context);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to find resources for " + packageName);
+ }
+ }
+ if (sPartner == null) {
+ sPartner = new Partner(null, null, null);
+ }
+ }
+ return sPartner;
+ }
+
+ /** Resets the Partner instance to handle the partner package has changed. */
+ public static void reset(Context context, String packageName) {
+ synchronized (sLock) {
+ if (sPartner != null && !TextUtils.isEmpty(packageName)) {
+ if (packageName.equals(sPartner.mPackageName)) {
+ // Force a refresh, so we send an Init to the updated package
+ sPartner = null;
+ getInstance(context);
+ }
+ }
+ }
+ }
+
+ /** This method is used to send init broadcast to the new/changed partner package. */
+ private void sendInitBroadcast(Context context) {
+ if (!TextUtils.isEmpty(mPackageName) && !TextUtils.isEmpty(mReceiverName)) {
+ Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION);
+ final ComponentName componentName = new ComponentName(mPackageName, mReceiverName);
+ intent.setComponent(componentName);
+ intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ context.sendBroadcast(intent);
+ }
+ }
+
+ /** Returns the order of inputs. */
+ public Map<Integer, Integer> getInputsOrderMap() {
+ HashMap<Integer, Integer> map = new HashMap<>();
+ if (mResources != null && !TextUtils.isEmpty(mPackageName)) {
+ String[] inputsArray = null;
+ final int resId = mResources.getIdentifier(INPUTS_ORDER, TYPE_ARRAY, mPackageName);
+ if (resId != 0) {
+ inputsArray = mResources.getStringArray(resId);
+ }
+ if (inputsArray != null) {
+ int priority = 0;
+ for (String input : inputsArray) {
+ Integer type = INPUT_TYPE_MAP.get(input);
+ if (type != null) {
+ map.put(type, priority++);
+ }
+ }
+ }
+ }
+ return map;
+ }
+
+ private static ResolveInfo getPartnerResolveInfo(PackageManager pm) {
+ final Intent intent = new Intent(ACTION_PARTNER_CUSTOMIZATION);
+ ResolveInfo partnerInfo = null;
+ for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
+ if (isSystemApp(info)) {
+ partnerInfo = info;
+ break;
+ }
+ }
+ return partnerInfo;
+ }
+
+ protected static boolean isSystemApp(ResolveInfo info) {
+ return (info.activityInfo != null
+ && info.activityInfo.applicationInfo != null
+ && (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
+ }
+}
diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java
index 453885a4..a355be99 100644
--- a/src/com/android/tv/util/PermissionUtils.java
+++ b/src/com/android/tv/util/PermissionUtils.java
@@ -47,4 +47,9 @@ public class PermissionUtils {
return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS)
== PackageManager.PERMISSION_GRANTED;
}
+
+ public static boolean hasInternet(Context context) {
+ return context.checkSelfPermission("android.permission.INTERNET")
+ == PackageManager.PERMISSION_GRANTED;
+ }
}
diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java
deleted file mode 100644
index 2c51d5a0..00000000
--- a/src/com/android/tv/util/PipInputManager.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.util;
-
-import android.content.Context;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
-import android.media.tv.TvInputManager.TvInputCallback;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.tv.ChannelTuner;
-import com.android.tv.R;
-import com.android.tv.data.Channel;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A class that manages inputs for PIP. All tuner inputs are represented to one tuner input for PIP.
- * Hidden inputs should not be visible to the users.
- */
-public class PipInputManager {
- private static final String TAG = "PipInputManager";
-
- // Tuner inputs aren't distinguished each other in PipInput. They are handled as one input.
- // Therefore, we define a fake input id for the unified input.
- private static final String TUNER_INPUT_ID = "tuner_input_id";
-
- private final Context mContext;
- private final TvInputManagerHelper mInputManager;
- private final ChannelTuner mChannelTuner;
- private boolean mStarted;
- private final Map<String, PipInput> mPipInputMap = new HashMap<>(); // inputId -> PipInput
- private final Set<Listener> mListeners = new ArraySet<>();
-
- private final TvInputCallback mTvInputCallback = new TvInputCallback() {
- @Override
- public void onInputAdded(String inputId) {
- TvInputInfo input = mInputManager.getTvInputInfo(inputId);
- if (input.isPassthroughInput()) {
- boolean available = mInputManager.getInputState(input)
- == TvInputManager.INPUT_STATE_CONNECTED;
- mPipInputMap.put(inputId, new PipInput(inputId, available));
- } else if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) {
- boolean available = mChannelTuner.getBrowsableChannelCount() != 0;
- mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available));
- } else {
- return;
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- @Override
- public void onInputRemoved(String inputId) {
- PipInput pipInput = mPipInputMap.remove(inputId);
- if (pipInput == null) {
- if (!mPipInputMap.containsKey(TUNER_INPUT_ID)) {
- Log.w(TAG, "A TV input (" + inputId + ") isn't tracked in PipInputManager");
- return;
- }
- if (mInputManager.getTunerTvInputSize() > 0) {
- return;
- }
- mPipInputMap.remove(TUNER_INPUT_ID);
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- @Override
- public void onInputStateChanged(String inputId, int state) {
- PipInput pipInput = mPipInputMap.get(inputId);
- if (pipInput == null) {
- // For tuner input, state change is handled in mChannelTunerListener.
- return;
- }
- pipInput.updateAvailability();
- }
- };
-
- private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() {
- @Override
- public void onLoadFinished() { }
-
- @Override
- public void onCurrentChannelUnavailable(Channel channel) { }
-
- @Override
- public void onBrowsableChannelListChanged() {
- PipInput tunerInput = mPipInputMap.get(TUNER_INPUT_ID);
- if (tunerInput == null) {
- return;
- }
- tunerInput.updateAvailability();
- }
-
- @Override
- public void onChannelChanged(Channel previousChannel, Channel currentChannel) {
- if (previousChannel != null && currentChannel != null
- && !previousChannel.isPassthrough() && !currentChannel.isPassthrough()) {
- // Channel change between channels for tuner inputs.
- return;
- }
- PipInput previousMainInput = getPipInput(previousChannel);
- if (previousMainInput != null) {
- previousMainInput.updateAvailability();
- }
- PipInput currentMainInput = getPipInput(currentChannel);
- if (currentMainInput != null) {
- currentMainInput.updateAvailability();
- }
- }
- };
-
- public PipInputManager(Context context, TvInputManagerHelper inputManager,
- ChannelTuner channelTuner) {
- mContext = context;
- mInputManager = inputManager;
- mChannelTuner = channelTuner;
- }
-
- /**
- * Starts {@link PipInputManager}.
- */
- public void start() {
- if (mStarted) {
- return;
- }
- mStarted = true;
- mInputManager.addCallback(mTvInputCallback);
- mChannelTuner.addListener(mChannelTunerListener);
- initializePipInputList();
- }
-
- /**
- * Stops {@link PipInputManager}.
- */
- public void stop() {
- if (!mStarted) {
- return;
- }
- mStarted = false;
- mInputManager.removeCallback(mTvInputCallback);
- mChannelTuner.removeListener(mChannelTunerListener);
- mPipInputMap.clear();
- }
-
- /**
- * Adds a {@link PipInputManager.Listener}.
- */
- public void addListener(Listener listener) {
- mListeners.add(listener);
- }
-
- /**
- * Removes a {@link PipInputManager.Listener}.
- */
- public void removeListener(Listener listener) {
- mListeners.remove(listener);
- }
-
- /**
- * Gets the size of inputs for PIP.
- *
- * <p>The hidden inputs are not counted.
- *
- * @param availableOnly If {@code true}, it counts only available PIP inputs. Please see {@link
- * PipInput#isAvailable()} for the details of availability.
- */
- public int getPipInputSize(boolean availableOnly) {
- int count = 0;
- for (PipInput pipInput : mPipInputMap.values()) {
- if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
- ++count;
- }
- if (pipInput.isPassthrough()) {
- TvInputInfo info = pipInput.getInputInfo();
- // Do not count HDMI ports if a CEC device is directly connected to the port.
- if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
- --count;
- }
- }
- }
- return count;
- }
-
- /**
- * Gets the list of inputs for PIP..
- *
- * <p>The hidden inputs are excluded.
- *
- * @param availableOnly If true, it returns only available PIP inputs. Please see {@link
- * PipInput#isAvailable()} for the details of availability.
- */
- public List<PipInput> getPipInputList(boolean availableOnly) {
- List<PipInput> pipInputs = new ArrayList<>();
- List<PipInput> removeInputs = new ArrayList<>();
- for (PipInput pipInput : mPipInputMap.values()) {
- if (!pipInput.isHidden() && (!availableOnly || pipInput.mAvailable)) {
- pipInputs.add(pipInput);
- }
- if (pipInput.isPassthrough()) {
- TvInputInfo info = pipInput.getInputInfo();
- // Do not show HDMI ports if a CEC device is directly connected to the port.
- if (info.getParentId() != null && !info.isConnectedToHdmiSwitch()) {
- removeInputs.add(mPipInputMap.get(info.getParentId()));
- }
- }
- }
- if (!removeInputs.isEmpty()) {
- pipInputs.removeAll(removeInputs);
- }
- Collections.sort(pipInputs, new Comparator<PipInput>() {
- @Override
- public int compare(PipInput lhs, PipInput rhs) {
- if (!lhs.mIsPassthrough) {
- return -1;
- }
- if (!rhs.mIsPassthrough) {
- return 1;
- }
- String a = lhs.getLabel();
- String b = rhs.getLabel();
- return a.compareTo(b);
- }
- });
- return pipInputs;
- }
-
- /**
- * Returns an PIP input corresponding to {@code channel}.
- */
- public PipInput getPipInput(Channel channel) {
- if (channel == null) {
- return null;
- }
- if (channel.isPassthrough()) {
- return mPipInputMap.get(channel.getInputId());
- } else {
- return mPipInputMap.get(TUNER_INPUT_ID);
- }
- }
-
- /**
- * Returns true, if {@code channel1} and {@code channel2} belong to the same input. For example,
- * two channels from different tuner inputs are also in the same input "Tuner" from PIP
- * point of view.
- */
- public boolean areInSamePipInput(Channel channel1, Channel channel2) {
- PipInput input1 = getPipInput(channel1);
- PipInput input2 = getPipInput(channel2);
- return input1 != null && input2 != null
- && getPipInput(channel1).equals(getPipInput(channel2));
- }
-
- private void initializePipInputList() {
- boolean hasTunerInput = false;
- for (TvInputInfo input : mInputManager.getTvInputInfos(false, false)) {
- if (input.isPassthroughInput()) {
- boolean available = mInputManager.getInputState(input)
- == TvInputManager.INPUT_STATE_CONNECTED;
- mPipInputMap.put(input.getId(), new PipInput(input.getId(), available));
- } else if (!hasTunerInput) {
- hasTunerInput = true;
- boolean available = mChannelTuner.getBrowsableChannelCount() != 0;
- mPipInputMap.put(TUNER_INPUT_ID, new PipInput(TUNER_INPUT_ID, available));
- }
- }
- PipInput input = getPipInput(mChannelTuner.getCurrentChannel());
- if (input != null) {
- input.updateAvailability();
- }
- for (Listener l : mListeners) {
- l.onPipInputListUpdated();
- }
- }
-
- /**
- * Listeners to notify PIP input state changes.
- */
- public interface Listener {
- /**
- * Called when the state (availability) of PIP inputs is changed.
- */
- void onPipInputStateUpdated();
-
- /**
- * Called when the list of PIP inputs is changed.
- */
- void onPipInputListUpdated();
- }
-
- /**
- * Input class for PIP. It has useful methods for PIP handling.
- */
- public class PipInput {
- private final String mInputId;
- private final boolean mIsPassthrough;
- private final TvInputInfo mInputInfo;
- private boolean mAvailable;
-
- private PipInput(String inputId, boolean available) {
- mInputId = inputId;
- mIsPassthrough = !mInputId.equals(TUNER_INPUT_ID);
- if (mIsPassthrough) {
- mInputInfo = mInputManager.getTvInputInfo(mInputId);
- } else {
- mInputInfo = null;
- }
- mAvailable = available;
- }
-
- /**
- * Returns the {@link TvInputInfo} object that matches to this PIP input.
- */
- public TvInputInfo getInputInfo() {
- return mInputInfo;
- }
-
- /**
- * Returns {@code true}, if the input is available for PIP. If a channel of an input is
- * already played or an input is not connected state or there is no browsable channel, the
- * input is unavailable.
- */
- public boolean isAvailable() {
- return mAvailable;
- }
-
- /**
- * Returns true, if the input is a passthrough TV input.
- */
- public boolean isPassthrough() {
- return mIsPassthrough;
- }
-
- /**
- * Gets a channel to play in a PIP view.
- */
- public Channel getChannel() {
- if (mIsPassthrough) {
- return Channel.createPassthroughChannel(mInputId);
- } else {
- return mChannelTuner.findNearestBrowsableChannel(
- Utils.getLastWatchedChannelId(mContext));
- }
- }
-
- /**
- * Gets a label of the input.
- */
- public String getLabel() {
- if (mIsPassthrough) {
- return mInputInfo.loadLabel(mContext).toString();
- } else {
- return mContext.getString(R.string.input_selector_tuner_label);
- }
- }
-
- /**
- * Gets a long label including a customized label.
- */
- public String getLongLabel() {
- if (mIsPassthrough) {
- String customizedLabel = Utils.loadLabel(mContext, mInputInfo);
- String label = getLabel();
- if (label.equals(customizedLabel)) {
- return customizedLabel;
- }
- return customizedLabel + " (" + label + ")";
- } else {
- return mContext.getString(R.string.input_long_label_for_tuner);
- }
- }
-
- /**
- * Updates availability. It returns true, if availability is changed.
- */
- private void updateAvailability() {
- boolean available;
- // current playing input cannot be available for PIP.
- Channel currentChannel = mChannelTuner.getCurrentChannel();
- if (mIsPassthrough) {
- if (currentChannel != null && currentChannel.getInputId().equals(mInputId)) {
- available = false;
- } else {
- available = mInputManager.getInputState(mInputId)
- == TvInputManager.INPUT_STATE_CONNECTED;
- }
- } else {
- if (currentChannel != null && !currentChannel.isPassthrough()) {
- available = false;
- } else {
- available = mChannelTuner.getBrowsableChannelCount() > 0;
- }
- }
- if (mAvailable != available) {
- mAvailable = available;
- for (Listener l : mListeners) {
- l.onPipInputStateUpdated();
- }
- }
- }
-
- private boolean isHidden() {
- // mInputInfo is null for the tuner input and it's always visible.
- return mInputInfo != null && mInputInfo.isHidden(mContext);
- }
- }
-}
diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java
index 4135bd4e..8b45131b 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 resetNextRunTime) {
SoftPreconditions.checkState(!mRunning, TAG, mName + " start is called twice.");
if (mRunning) {
return;
}
mRunning = true;
+ if (resetNextRunTime) {
+ resetNextRunTime();
+ }
new AsyncTask<Void, Void, Long>() {
@Override
protected Long doInBackground(Void... params) {
@@ -76,6 +79,10 @@ public final class RecurringRunner {
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
+ public void start() {
+ start(false);
+ }
+
public void stop() {
mRunning = false;
mHandler.removeCallbacksAndMessages(null);
diff --git a/src/com/android/tv/util/SearchManagerHelper.java b/src/com/android/tv/util/SearchManagerHelper.java
deleted file mode 100644
index b6e34d7a..00000000
--- a/src/com/android/tv/util/SearchManagerHelper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tv.util;
-
-import android.app.SearchManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import java.lang.reflect.InvocationTargetException;
-
-/**
- * A convenience class for calling methods in android.app.SearchManager.
- */
-public final class SearchManagerHelper {
- private static final String TAG = "SearchManagerHelper";
-
- private static final Object sLock = new Object();
- private static SearchManagerHelper sInstance;
-
- private final SearchManager mSearchManager;
-
- private SearchManagerHelper(Context context) {
- mSearchManager = ((android.app.SearchManager) context.getSystemService(
- Context.SEARCH_SERVICE));
- }
-
- public static SearchManagerHelper getInstance(Context context) {
- synchronized (sLock) {
- if (sInstance == null) {
- sInstance = new SearchManagerHelper(context.getApplicationContext());
- }
- return sInstance;
- }
- }
-
- public void launchAssistAction() {
- try {
- SearchManager.class.getDeclaredMethod("launchLegacyAssist", String.class, Integer.TYPE,
- Bundle.class).invoke(mSearchManager, null, UserHandle.myUserId(), null);
- } catch (NoSuchMethodException | IllegalArgumentException | IllegalAccessException
- | InvocationTargetException e) {
- Log.e(TAG, "Fail to call SearchManager.launchAssistAction", e);
- }
- }
-}
diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java
index 8223a81c..32e3a81f 100644
--- a/src/com/android/tv/util/SetupUtils.java
+++ b/src/com/android/tv/util/SetupUtils.java
@@ -37,8 +37,6 @@ import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.Channel;
import com.android.tv.data.ChannelDataManager;
-import com.android.tv.data.epg.EpgFetcher;
-import com.android.tv.experiments.Experiments;
import com.android.tv.tuner.tvinput.TunerTvInputService;
import java.util.Collections;
@@ -114,7 +112,7 @@ public class SetupUtils {
@Override
public void onLoadFinished() {
manager.removeListener(this);
- updateChannelBrowsable(mTvApplication, inputId, postRunnable);
+ updateChannelsAfterSetup(mTvApplication, inputId, postRunnable);
}
@Override
@@ -124,17 +122,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 +141,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();
@@ -382,13 +387,5 @@ public class SetupUtils {
mSetUpInputs.add(inputId);
mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
}
- // Start fetching program guide data for internal tuners.
- Context context = mTvApplication.getApplicationContext();
- if (Utils.isInternalTvInput(context, inputId)) {
- if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
- == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) {
- EpgFetcher.getInstance(context).startImmediately();
- }
- }
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/util/StringUtils.java b/src/com/android/tv/util/StringUtils.java
index 15571e75..659807e2 100644
--- a/src/com/android/tv/tuner/util/StringUtils.java
+++ b/src/com/android/tv/util/StringUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.tv.tuner.util;
+package com.android.tv.util;
/**
* Utility class for handling {@link String}.
diff --git a/src/com/android/tv/util/TimeShiftUtils.java b/src/com/android/tv/util/TimeShiftUtils.java
index 238d0e74..8038a78f 100644
--- a/src/com/android/tv/util/TimeShiftUtils.java
+++ b/src/com/android/tv/util/TimeShiftUtils.java
@@ -18,7 +18,6 @@ package com.android.tv.util;
import java.util.concurrent.TimeUnit;
-// TODO: move related functions in TimeShiftManger here.
/**
* A class that includes convenience methods for time shift plays.
*/
@@ -40,7 +39,7 @@ public class TimeShiftUtils {
* Returns real speeds used in time shift play. This method is only for fast-forwarding and
* rewinding. The normal play speed is not addressed here.
*
- * @param speedLevel the valid value is ranged from 0 to {@link MAX_SPPED_LEVEL}.
+ * @param speedLevel the valid value is ranged from 0 to {@link #MAX_SPEED_LEVEL}.
* @param programDurationMillis the length of program under playing.
* @throws IndexOutOfBoundsException if speed level is out of its range.
*/
@@ -60,4 +59,3 @@ public class TimeShiftUtils {
: SHORT_PROGRAM_SPEED_FACTORS[MAX_SPEED_LEVEL];
}
}
-
diff --git a/src/com/android/tv/util/TvInputManagerHelper.java b/src/com/android/tv/util/TvInputManagerHelper.java
index 121f56ed..730a985b 100644
--- a/src/com/android/tv/util/TvInputManagerHelper.java
+++ b/src/com/android/tv/util/TvInputManagerHelper.java
@@ -18,20 +18,26 @@ package com.android.tv.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.hardware.hdmi.HdmiDeviceInfo;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputManager.TvInputCallback;
import android.os.Handler;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.Log;
import com.android.tv.Features;
import com.android.tv.common.SoftPreconditions;
+import com.android.tv.common.TvCommonUtils;
import com.android.tv.parental.ContentRatingsManager;
import com.android.tv.parental.ParentalControlSettings;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -42,14 +48,64 @@ import java.util.Map;
public class TvInputManagerHelper {
private static final String TAG = "TvInputManagerHelper";
private static final boolean DEBUG = false;
+
+ /**
+ * Types of HDMI device and bundled tuner.
+ */
+ public static final int TYPE_CEC_DEVICE = -2;
+ public static final int TYPE_BUNDLED_TUNER = -3;
+ public static final int TYPE_CEC_DEVICE_RECORDER = -4;
+ public static final int TYPE_CEC_DEVICE_PLAYBACK = -5;
+ public static final int TYPE_MHL_MOBILE = -6;
+
+ private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
+ "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
+ private static final String [] mPhysicalTunerBlackList = {
+ };
+ private static final String META_LABEL_SORT_KEY = "input_sort_key";
+
+ /**
+ * The default tv input priority to show.
+ */
+ private static final ArrayList<Integer> DEFAULT_TV_INPUT_PRIORITY = new ArrayList<>();
+ static {
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_BUNDLED_TUNER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_TUNER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_RECORDER);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_CEC_DEVICE_PLAYBACK);
+ DEFAULT_TV_INPUT_PRIORITY.add(TYPE_MHL_MOBILE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_HDMI);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DVI);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPONENT);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SVIDEO);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_COMPOSITE);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_DISPLAY_PORT);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_VGA);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_SCART);
+ DEFAULT_TV_INPUT_PRIORITY.add(TvInputInfo.TYPE_OTHER);
+ }
+
private static final String[] PARTNER_TUNER_INPUT_PREFIX_BLACKLIST = {
};
+ private static final String[] TESTABLE_INPUTS = {
+ "com.android.tv.testinput/.TestTvInputService"
+ };
+
private final Context mContext;
+ private final PackageManager mPackageManager;
private final TvInputManager mTvInputManager;
private final Map<String, Integer> mInputStateMap = new HashMap<>();
private final Map<String, TvInputInfo> mInputMap = new HashMap<>();
+ private final Map<String, String> mTvInputLabels = new ArrayMap<>();
+ private final Map<String, String> mTvInputCustomLabels = new ArrayMap<>();
private final Map<String, Boolean> mInputIdToPartnerInputMap = new HashMap<>();
+
+ private final Map<String, CharSequence> mTvInputApplicationLabels = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputApplicationIcons = new ArrayMap<>();
+ private final Map<String, Drawable> mTvInputAppliactionBanners = new ArrayMap<>();
+
private final TvInputCallback mInternalCallback = new TvInputCallback() {
@Override
public void onInputStateChanged(String inputId, int state) {
@@ -72,6 +128,11 @@ public class TvInputManagerHelper {
TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
if (info != null) {
mInputMap.put(inputId, info);
+ mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
+ }
mInputStateMap.put(inputId, mTvInputManager.getInputState(inputId));
mInputIdToPartnerInputMap.put(inputId, isPartnerInput(info));
}
@@ -85,6 +146,11 @@ public class TvInputManagerHelper {
public void onInputRemoved(String inputId) {
if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
mInputMap.remove(inputId);
+ mTvInputLabels.remove(inputId);
+ mTvInputCustomLabels.remove(inputId);
+ mTvInputApplicationLabels.remove(inputId);
+ mTvInputApplicationIcons.remove(inputId);
+ mTvInputAppliactionBanners.remove(inputId);
mInputStateMap.remove(inputId);
mInputIdToPartnerInputMap.remove(inputId);
mContentRatingsManager.update();
@@ -103,6 +169,14 @@ public class TvInputManagerHelper {
}
TvInputInfo info = mTvInputManager.getTvInputInfo(inputId);
mInputMap.put(inputId, info);
+ mTvInputLabels.put(inputId, info.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = info.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputId, inputCustomLabel.toString());
+ }
+ mTvInputApplicationLabels.remove(inputId);
+ mTvInputApplicationIcons.remove(inputId);
+ mTvInputAppliactionBanners.remove(inputId);
for (TvInputCallback callback : mCallbacks) {
callback.onInputUpdated(inputId);
}
@@ -114,6 +188,11 @@ public class TvInputManagerHelper {
public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
if (DEBUG) Log.d(TAG, "onTvInputInfoUpdated " + inputInfo);
mInputMap.put(inputInfo.getId(), inputInfo);
+ mTvInputLabels.put(inputInfo.getId(), inputInfo.loadLabel(mContext).toString());
+ CharSequence inputCustomLabel = inputInfo.loadCustomLabel(mContext);
+ if (inputCustomLabel != null) {
+ mTvInputCustomLabels.put(inputInfo.getId(), inputCustomLabel.toString());
+ }
for (TvInputCallback callback : mCallbacks) {
callback.onTvInputInfoUpdated(inputInfo);
}
@@ -131,13 +210,18 @@ 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);
- mTvInputInfoComparator = new TvInputInfoComparator(this);
+ mTvInputInfoComparator = new InputComparatorInternal(this);
}
public void start() {
+ if (!hasTvInputManager()) {
+ // Not a TV device
+ return;
+ }
if (mStarted) {
return;
}
@@ -145,6 +229,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 +260,23 @@ public class TvInputManagerHelper {
mStarted = false;
mInputStateMap.clear();
mInputMap.clear();
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ mTvInputApplicationIcons.clear();
+ mTvInputAppliactionBanners.clear();;
mInputIdToPartnerInputMap.clear();
}
+ /**
+ * Clears the TvInput labels map.
+ */
+ public void clearTvInputLabels() {
+ mTvInputLabels.clear();
+ mTvInputCustomLabels.clear();
+ mTvInputApplicationLabels.clear();
+ }
+
public List<TvInputInfo> getTvInputInfos(boolean availableOnly, boolean tunerOnly) {
ArrayList<TvInputInfo> list = new ArrayList<>();
for (Map.Entry<String, Integer> pair : mInputStateMap.entrySet()) {
@@ -192,7 +295,7 @@ public class TvInputManagerHelper {
/**
* Returns the default comparator for {@link TvInputInfo}.
- * See {@link TvInputInfoComparator} for detail.
+ * See {@link InputComparatorInternal} for detail.
*/
public Comparator<TvInputInfo> getDefaultTvInputInfoComparator() {
return mTvInputInfoComparator;
@@ -237,15 +340,81 @@ public class TvInputManagerHelper {
}
/**
- * Loads label of {@code info}.
+ * Is (Context.TV_INPUT_SERVICE) available.
*
- * It's visible for comparator test to mock TvInputInfo.
- * Package private is enough for this method, but public is necessary to workaround mockito
- * bug.
+ * <p>This is only available on TV devices.
+ */
+ public boolean hasTvInputManager() {
+ return mTvInputManager != null;
+ }
+
+ /**
+ * Loads label of {@code info}.
*/
- @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,14 +490,54 @@ 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;
}
@@ -342,10 +551,10 @@ public class TvInputManagerHelper {
* (i.e. Mockito's spy doesn't work)
*/
@VisibleForTesting
- static class TvInputInfoComparator implements Comparator<TvInputInfo> {
+ static class InputComparatorInternal implements Comparator<TvInputInfo> {
private final TvInputManagerHelper mInputManager;
- public TvInputInfoComparator(TvInputManagerHelper inputManager) {
+ public InputComparatorInternal(TvInputManagerHelper inputManager) {
mInputManager = inputManager;
}
@@ -357,4 +566,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 HardwareInputComparator implements Comparator<TvInputInfo> {
+ private Map<Integer, Integer> mTypePriorities = new HashMap<>();
+ private final TvInputManagerHelper mTvInputManagerHelper;
+ private final Context mContext;
+
+ public HardwareInputComparator(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..c5fde317 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,53 +28,27 @@ 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.
*/
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,58 +65,7 @@ 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();
- }
+ private TvSettings() {}
// Multi-track audio settings
public static String getMultiAudioId(Context context) {
@@ -173,26 +98,61 @@ public final class TvSettings {
PREF_MULTI_AUDIO_CHANNEL_COUNT, channelCount).apply();
}
- // Parental Control settings
- public static void addContentRatingSystems(Context context, Set<String> ids) {
- Set<String> contentRatingSystemSet = getContentRatingSystemSet(context);
- if (contentRatingSystemSet.addAll(ids)) {
- PreferenceManager.getDefaultSharedPreferences(context).edit()
- .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).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 void addContentRatingSystem(Context context, String id) {
- Set<String> contentRatingSystemSet = getContentRatingSystemSet(context);
- if (contentRatingSystemSet.add(id)) {
- PreferenceManager.getDefaultSharedPreferences(context).edit()
- .putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).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;
}
}
- public static void removeContentRatingSystems(Context context, Set<String> ids) {
+ // Parental Control settings
+ public static void addContentRatingSystem(Context context, String id) {
Set<String> contentRatingSystemSet = getContentRatingSystemSet(context);
- if (contentRatingSystemSet.removeAll(ids)) {
+ if (contentRatingSystemSet.add(id)) {
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putStringSet(PREF_CONTENT_RATING_SYSTEMS, contentRatingSystemSet).apply();
}
@@ -254,4 +214,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/TvProviderUriMatcher.java b/src/com/android/tv/util/TvUriMatcher.java
index 749e4aa3..3d91cdad 100644
--- a/src/com/android/tv/util/TvProviderUriMatcher.java
+++ b/src/com/android/tv/util/TvUriMatcher.java
@@ -16,23 +16,27 @@
package com.android.tv.util;
+import android.app.SearchManager;
import android.content.UriMatcher;
import android.media.tv.TvContract;
import android.net.Uri;
import android.support.annotation.IntDef;
+import com.android.tv.search.LocalSearchProvider;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Utility class to aid in matching URIs in TvProvider.
*/
-public class TvProviderUriMatcher {
+public class TvUriMatcher {
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
@Retention(RetentionPolicy.SOURCE)
@IntDef({MATCH_CHANNEL, MATCH_CHANNEL_ID, MATCH_PROGRAM, MATCH_PROGRAM_ID,
- MATCH_RECORDED_PROGRAM, MATCH_RECORDED_PROGRAM_ID, MATCH_WATCHED_PROGRAM_ID})
+ MATCH_RECORDED_PROGRAM, MATCH_RECORDED_PROGRAM_ID, MATCH_WATCHED_PROGRAM_ID,
+ MATCH_ON_DEVICE_SEARCH})
private @interface TvProviderUriMatchCode {}
/** The code for the channels URI. */
public static final int MATCH_CHANNEL = 1;
@@ -48,6 +52,8 @@ public class TvProviderUriMatcher {
public static final int MATCH_RECORDED_PROGRAM_ID = 6;
/** The code for the watched program URI. */
public static final int MATCH_WATCHED_PROGRAM_ID = 7;
+ /** The code for the on-device search URI. */
+ public static final int MATCH_ON_DEVICE_SEARCH = 8;
static {
URI_MATCHER.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
URI_MATCHER.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
@@ -56,9 +62,11 @@ public class TvProviderUriMatcher {
URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
URI_MATCHER.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
URI_MATCHER.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
+ URI_MATCHER.addURI(LocalSearchProvider.AUTHORITY,
+ SearchManager.SUGGEST_URI_PATH_QUERY + "/*", MATCH_ON_DEVICE_SEARCH);
}
- private TvProviderUriMatcher() { }
+ private TvUriMatcher() { }
/**
* Try to match against the path in a url.
diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java
index 99d34431..d11bab3c 100644
--- a/src/com/android/tv/util/Utils.java
+++ b/src/com/android/tv/util/Utils.java
@@ -31,6 +31,7 @@ import android.media.tv.TvContract.Programs.Genres;
import android.media.tv.TvInputInfo;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
+import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
@@ -44,11 +45,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;
@@ -62,6 +65,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
@@ -74,7 +79,6 @@ public class Utils {
private static final SimpleDateFormat ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",
Locale.US);
- public static final String EXTRA_KEY_KEYCODE = "keycode";
public static final String EXTRA_KEY_ACTION = "action";
public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input";
public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher";
@@ -83,11 +87,9 @@ 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";
+ private static final String PATH_RECORDED_PROGRAM = "recorded_program";
private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID = "last_watched_channel_id";
private static final String PREF_KEY_LAST_WATCHED_CHANNEL_ID_FOR_INPUT =
@@ -97,6 +99,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 +118,7 @@ public class Utils {
private static final int AUDIO_CHANNEL_SURROUND_8 = 8;
private static final long RECORDING_FAILED_REASON_NONE = 0;
+ private static final long HALF_MINUTE_MS = TimeUnit.SECONDS.toMillis(30);
private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
// Hardcoded list for known bundled inputs not written by OEM/SOCs.
@@ -207,6 +212,28 @@ public class Utils {
}
/**
+ * Adds the info of failed scheduled recording.
+ */
+ public static void addFailedScheduledRecordingInfo(Context context,
+ String scheduledRecordingInfo) {
+ Set<String> failedScheduledRecordingInfoSet = getFailedScheduledRecordingInfoSet(context);
+ failedScheduledRecordingInfoSet.add(scheduledRecordingInfo);
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .putStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET,
+ failedScheduledRecordingInfoSet)
+ .apply();
+ }
+
+ /**
+ * Clears the failed scheduled recording info set.
+ */
+ public static void clearFailedScheduledRecordingInfoSet(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit()
+ .remove(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET)
+ .apply();
+ }
+
+ /**
* Clears recording failed reason.
*/
public static void clearRecordingFailedReason(Context context, int reason) {
@@ -246,6 +273,14 @@ public class Utils {
}
/**
+ * Returns the failed scheduled recordings info set.
+ */
+ public static Set<String> getFailedScheduledRecordingInfoSet(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getStringSet(PREF_KEY_FAILED_SCHEDULED_RECORDING_INFO_SET, new HashSet<>());
+ }
+
+ /**
* Checks do recording failed reason exist.
*/
public static boolean hasRecordingFailedReason(Context context, int reason) {
@@ -296,6 +331,13 @@ public class Utils {
}
/**
+ * Returns {@code true}, if {@code uri} is a programs URI.
+ */
+ public static boolean isRecordedProgramsUri(Uri uri) {
+ return isTvUri(uri) && PATH_RECORDED_PROGRAM.equals(uri.getPathSegments().get(0));
+ }
+
+ /**
* Gets the info of the program on particular time.
*/
@WorkerThread
@@ -333,6 +375,14 @@ public class Utils {
}
/**
+ * Returns the round off minutes when convert milliseconds to minutes.
+ */
+ public static int getRoundOffMinsFromMs(long millis) {
+ // Round off the result by adding half minute to the original ms.
+ return (int) TimeUnit.MILLISECONDS.toMinutes(millis + HALF_MINUTE_MS);
+ }
+
+ /**
* Returns duration string according to the date & time format.
* If {@code startUtcMillis} and {@code endUtcMills} are equal,
* formatted time will be returned instead.
@@ -392,16 +442,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 +575,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 {
@@ -606,10 +658,12 @@ public class Utils {
if (input == null) {
return null;
}
- CharSequence customLabel = input.loadCustomLabel(context);
+ TvInputManagerHelper inputManager =
+ TvApplication.getSingletons(context).getTvInputManagerHelper();
+ CharSequence customLabel = inputManager.loadCustomLabel(input);
String label = (customLabel == null) ? null : customLabel.toString();
if (TextUtils.isEmpty(label)) {
- label = input.loadLabel(context).toString();
+ label = inputManager.loadLabel(input).toString();
}
return label;
}
@@ -860,4 +914,28 @@ 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();
+ }
+
+ /**
+ * Runs the method in main thread. If the current thread is not main thread, block it util
+ * the method is finished.
+ */
+ public static void runInMainThreadAndWait(Runnable runnable) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ Future<?> temp = MainThreadExecutor.getInstance().submit(runnable);
+ try {
+ temp.get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(TAG, "failed to finish the execution", e);
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/util/ViewCache.java b/src/com/android/tv/util/ViewCache.java
new file mode 100644
index 00000000..ed9a8ff6
--- /dev/null
+++ b/src/com/android/tv/util/ViewCache.java
@@ -0,0 +1,100 @@
+package com.android.tv.util;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * A cache for the views.
+ */
+public class ViewCache {
+ private final static SparseArray<ArrayList<View>> mViews = new SparseArray();
+
+ private static ViewCache sViewCache;
+
+ private ViewCache() { }
+
+ /**
+ * Returns an instance of the view cache.
+ */
+ public static ViewCache getInstance() {
+ if (sViewCache == null) {
+ sViewCache = new ViewCache();
+ }
+ return sViewCache;
+ }
+
+ /**
+ * Returns if the view cache is empty.
+ */
+ public boolean isEmpty() {
+ return mViews.size() == 0;
+ }
+
+ /**
+ * Stores a view into this view cache.
+ */
+ public void putView(int resId, View view) {
+ ArrayList<View> views = mViews.get(resId);
+ if (views == null) {
+ views = new ArrayList();
+ mViews.put(resId, views);
+ }
+ views.add(view);
+ }
+
+ /**
+ * Stores multi specific views into the view cache.
+ */
+ public void putView(Context context, int resId, ViewGroup fakeParent, int num) {
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ ArrayList<View> views = mViews.get(resId);
+ if (views == null) {
+ views = new ArrayList<>();
+ mViews.put(resId, views);
+ }
+ for (int i = 0; i < num; i++) {
+ View view = inflater.inflate(resId, fakeParent, false);
+ views.add(view);
+ }
+ }
+
+ /**
+ * Returns the view for specific resource id.
+ */
+ public View getView(int resId) {
+ ArrayList<View> views = mViews.get(resId);
+ if (views != null && !views.isEmpty()) {
+ View view = views.remove(views.size() - 1);
+ if (views.isEmpty()) {
+ mViews.remove(resId);
+ }
+ return view;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the view if exists, or create a new view for the specific resource id.
+ */
+ public View getOrCreateView(LayoutInflater inflater, int resId, ViewGroup container) {
+ View view = getView(resId);
+ if (view == null) {
+ view = inflater.inflate(resId, container, false);
+ }
+ return view;
+ }
+
+ /**
+ * Clears the view cache.
+ */
+ public void clear() {
+ mViews.clear();
+ }
+}